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'] }
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 IChemin
.
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 (otherChemin
s orParam
s), this is what was passed to thechemin
function except that strings are converted topConstant
.match(pathname)
: test a chemin against a pathname, seematch
for more details.matchExact(pathname)
: test a chemin against a pathname for an exact match, seematchExact
for more details.stringify(params?, options?)
: serialize a chemin, seestringify
for more details.serialize(params?, options?)
: serialize a chemin, seeserialize
for more details.extract()
: return an array of all theChemin
it contains (as well as theChemin
itself), seeextract
for more details.flatten()
: return all theParam
it contains, seeflatten
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
(defaulttrue
): Add a slash at the beginingtrailingSlash
(default:false
): Add a slash at the end
Test a chemin against a pathname
Returns null
or ICheminMatch
.
pathname
can be either a string (/admin/user/5
) or an array of strings (['admin', 'user', '5']
)ICheminMatch
is an object with three propertiesrest
: 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 (ifrest
is empty or not)params
: an object of params extracted from the matching
Note: 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
(defaulttrue
): Add a slash at the beginingtrailingSlash
(default:false
): Add a slash at the end
const 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
(defaulttrue
): Add a slash at the beginingtrailingSlash
(default:false
): Add a slash at the end