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)/editThe 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, "/"); // falseMake any
Paramoptional
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 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 (otherChemins orParams), this is what was passed to thecheminfunction except that strings are converted topConstant.match(pathname): test a chemin against a pathname, seematchfor more details.matchExact(pathname): test a chemin against a pathname for an exact match, seematchExactfor more details.stringify(params?, options?): serialize a chemin, seestringifyfor more details.serialize(params?, options?): serialize a chemin, seeserializefor more details.extract(): return an array of all theCheminit contains (as well as theCheminitself), seeextractfor more details.flatten(): return all theParamit contains, seeflattenfor 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
Cheminor not
Accepts one argument and return true if it's a Chemin, false otherwise.
isChemin(chemin("admin")); // trueThe 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 TCheminMatch.
pathnamecan be either a string (/admin/user/5) or an array of strings (['admin', 'user', '5'])TCheminMatchis 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 (ifrestis 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"); // nullAccepts 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/editSplit 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
Cheminand apathnamereturn an new object with the result ofmatchfor 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
matchAllbut also match nested objects
Return an array of all the
Cheminit contains (as well as theCheminitself).
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)/editThe option object accepts two boolean properties:
leadingSlash(defaulttrue): Add a slash at the beginingtrailingSlash(default:false): Add a slash at the end