A type-safe pattern builder & route matching library written in TypeScript
import { chemin, pNumber, pOptionalConst } from "@dldc/chemin"; // admin/post/:postId(number)/delete? const path = chemin( "admin", "post", pNumber("postId"), pOptionalConst("delete"), ); console.log(path.match("/no/valid")); // => null const match = path.match("/admin/post/45"); console.log(match); // => { rest: [], exact: true, params: { postId: 45, delete: false } } // match.params is typed as { postId: number, delete: boolean } !
You can use a Chemin
inside another one to easily compose your routes !
import { chemin, pNumber, pString } from "@dldc/chemin"; const postFragment = chemin("post", pNumber("postId")); const postAdmin = chemin("admin", pString("userId"), postFragment, "edit"); console.log(postAdmin.stringify()); // /admin/:userId/post/:postId(number)/edit
The following params are build-in and exported from @dldc/chemin
.
A number using
parseFloat(x)
const chemin = chemin(pNumber("myNum")); matchExact(chemin, "/3.1415"); // { myNum: 3.1415 }
NOTE: Because it uses parseFloat
this will also accept Infinity
,
10e2
...
A integer using
parseInt(x, 10)
const chemin = chemin(pInteger("myInt")); matchExact(chemin, "/42"); // { myInt: 42 }
The options
parameter is optional and accepts a strict
boolean property
(true
by default). When strict is set to true
(the default) it will only
match if the parsed number is the same as the raw value (so 1.0
or 42blabla
will not match).
const chemin = chemin(pInteger("myInt", { strict: false })); matchExact(chemin, "/42fooo"); // { myInt: 42 }
Any non-empty string
const chemin = chemin(pString("myStr")); matchExact(chemin, "/cat"); // { myStr: 'cat' }
A constant string
const chemin = chemin(pConstant("edit")); matchExact(chemin, "/edit"); // {} matchExact(chemin, "/"); // false
Make any
Param
optional
const chemin = chemin(pOptional(pInteger("myInt"))); matchExact(chemin, "/42"); // { myInt: { present: true, value: 42 } } matchExact(chemin, "/"); // { myInt: { present: false } }
An optional contant string
const chemin = chemin(pOptionalConst("isEditing", "edit")); matchExact(chemin, "/edit"); // { isEditing: true } matchExact(chemin, "/"); // { isEditing: false }
If path
is omitted then the name is used as the path.
const chemin = chemin(pOptionalConst("edit")); matchExact(chemin, "/edit"); // { edit: true } matchExact(chemin, "/"); // { edit: false }
An optional string parameter
const chemin = chemin(pOptionalString("name")); matchExact(chemin, "/paul"); // { name: 'paul' } matchExact(chemin, "/"); // { name: false }
Allow a params to be repeated any number of time
const chemin = chemin(pMultiple(pString("categories"))); matchExact(chemin, "/"); // { categories: [] } matchExact(chemin, "/foo/bar"); // { categories: ['foo', 'bar'] }
const chemin = chemin(pMultiple(pString("categories"), true)); matchExact(chemin, "/"); // false because atLeastOne is true matchExact(chemin, "/foo/bar"); // { categories: ['foo', 'bar'] }
Param
You can create your own Param
to better fit your application while keeping
full type-safety !
import { chemin, type TCheminParam } from "@dldc/chemin"; // match only string of 4 char [a-z0-9] function pFourCharStringId<N extends string>(name: N): TCheminParam<N, string> { const reg = /^[a-z0-9]{4}$/; return { factory: pFourCharStringId, name, meta: null, isEqual: (other) => other.name === name, match: (...all) => { if (all[0].match(reg)) { return { match: true, value: all[0], next: all.slice(1) }; } return { match: false, next: all }; }, serialize: (value) => value, stringify: () => `:${name}(id4)`, }; } const path = chemin("item", pFourCharStringId("itemId")); console.log(path.match("/item/a4e3t")); // null (5 char) console.log(path.match("/item/A4e3")); // null (because A is uppercase) console.log(path.match("/item/a4e3")); // { rest: [], exact: true, params: { itemId: 'a4e3' } }
Take a look a the custom-advanced.test.ts example. and the build-in Params.
Create a
Chemin
Accepts any number or arguments of type string
, TCheminParam
or TChemin
.
Note: strings are converted to pConstant
.
chemin("admin", pNumber("userId"), pOptionalConst("edit"));
The chemin
function returns an object with the following properties:
parts
: an array of the parts (other Chemin
s or Param
s), this is what was
passed to the chemin
function except that strings are converted to
pConstant
.match(pathname)
: test a chemin against a pathname, see match
for more
details.matchExact(pathname)
: test a chemin against a pathname for an exact match,
see matchExact
for more details.stringify(params?, options?)
: serialize a chemin, see stringify
for more
details.serialize(params?, options?)
: serialize a chemin, see serialize
for more
details.extract()
: return an array of all the Chemin
it contains (as well as the
Chemin
itself), see extract
for more details.flatten()
: return all the Param
it contains, see flatten
for more
details.Note: Most of these functions are also exported as standalone functions (see
below). The only difference is that extract
and flatten
are cached when
called on a Chemin
itself, but you should rarely need to use them anyway.
Test wether an object is a
Chemin
or not
Accepts one argument and return true
if it's a Chemin
, false otherwise.
isChemin(chemin("admin")); // true
The cheminFactory
function returns a function that works exactly like chemin
but with a default serialize
/ stringify
options.
The defaultSerializeOptions
parameter is optional and accepts two boolean
properties:
leadingSlash
(default true
): Add a slash at the beginingtrailingSlash
(default: false
): Add a slash at the endTest a chemin against a pathname
Returns null
or TCheminMatch
.
pathname
can be either a string (/admin/user/5
) or an array of strings
(['admin', 'user', '5']
)TCheminMatch
is an object with three properties
rest
: an array of string of the remaining parts of the pathname once the
matching is doneexact
: a boolean indicating if the match is exact or not (if rest
is
empty or not)params
: an object of params extracted from the matchingNote: When pathname
is a string
, it is splitted using the
splitPathname
function. This function is exported so you can use it to split
your pathnames in the same way.
import { chemin, match, pNumber, pOptionalConst } from "@dldc/chemin"; const chemin = chemin("admin", pNumber("userId"), pOptionalConst("edit")); match(chemin, "/admin/42/edit"); // { rest: [], exact: true, params: { userId: 42, edit: true } } match(chemin, "/admin/42/edit/rest"); // { rest: ['rest'], exact: false, params: { userId: 42, edit: true } } match(chemin, "/noop"); // null
Accepts the same arguments as match
but return null
if the path does not
match or if rest
is not empty, otherwise it returns the params
object
directly.
Print a chemin from its params.
Accepts a chemin
some params
(an object or null
) and an optional option
object.
The option object accepts two boolean
properties:
leadingSlash
(default true
): Add a slash at the beginingtrailingSlash
(default: false
): Add a slash at the endconst chemin = chemin("admin", pNumber("userId"), pOptionalConst("edit")); serialize(chemin, { userId: 42, edit: true }); // /admin/42/edit
Split a pathname and prevent empty parts
Accepts a string and returns an array of strings.
splitPathname("/admin/user/5"); // ['admin', 'user', '5']
This function let you extract the params of a chemin that is part of another one
const workspaceBase = chemin("workspace", pString("tenant")); const routes = [ chemin("home"), // home chemin("settings"), // settings chemin(workspaceBase, "home"), // workspace home chemin(workspaceBase, "settings"), // workspace settings ]; function app(pathname: string) { const route = matchFirst(routes, pathname); if (!route) { return { route: null }; } const { chemin, match } = route; // extract the tenant from the workspace if it's a workspace route const params = partialMatch(chemin, match, workspaceBase); // params is typed as { tenant: string } | null if (params) { return { tenant: params.tenant, route: chemin.stringify() }; } return { route: chemin.stringify() }; }
Note: This is based on reference equality so it will not work if you create
a new Chemin
with the same parts: chemin('workspace', pString('tenant'))
!
Note 2: In reality this function simply returns the match.params
object if
the part
is contained in chemin
or null
otherwise. This mean that you
might get more properties that what the type gives you (but this is quite
commoin in TypeScript).
Given an object of
Chemin
and apathname
return an new object with the result ofmatch
for each keys
const chemins = { home: chemin("home"), workspace: chemin("workspace", pString("tenant")), workspaceSettings: chemin("workspace", pString("tenant"), "settings"), }; const match = matchAll(chemins, "/workspace/123/settings"); expect(match).toEqual({ home: null, workspace: { rest: ["settings"], exact: false, params: { tenant: "123" } }, workspaceSettings: { rest: [], exact: true, params: { tenant: "123" } }, });
Same as
matchAll
but also match nested objects
Return an array of all the
Chemin
it contains (as well as theChemin
itself).
import { Chemin } from "@dldc/chemin"; const admin = chemin("admin"); const adminUser = chemin(admin, "user"); adminUser.extract(); // [adminUser, admin];
Note: You probably don't need this but it's used internally in
partialMatch
Return a string representation of the chemin.
import { Chemin, pNumber, pString, stringify } from "@dldc/chemin"; const postFragment = chemin("post", pNumber("postId")); const postAdmin = chemin("admin", pString("userId"), postFragment, "edit"); console.log(stringify(postAdmin)); // /admin/:userId/post/:postId(number)/edit
The option object accepts two boolean
properties:
leadingSlash
(default true
): Add a slash at the beginingtrailingSlash
(default: false
): Add a slash at the endAdd Package
deno add jsr:@dldc/chemin
Import symbol
import * as chemin from "@dldc/chemin";
---- OR ----
Import directly with a jsr specifier
import * as chemin from "jsr:@dldc/chemin";
Add Package
npx jsr add @dldc/chemin
Import symbol
import * as chemin from "@dldc/chemin";
Add Package
yarn dlx jsr add @dldc/chemin
Import symbol
import * as chemin from "@dldc/chemin";
Add Package
pnpm dlx jsr add @dldc/chemin
Import symbol
import * as chemin from "@dldc/chemin";
Add Package
bunx jsr add @dldc/chemin
Import symbol
import * as chemin from "@dldc/chemin";