chemin logo

# 🥾 Chemin > A type-safe pattern builder & route matching library written in TypeScript ## Gist ```js 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 } ! ``` ## Composition You can use a `Chemin` inside another one to easily compose your routes ! ```ts 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 ``` ## Build-in params The following params are build-in and exported from `@dldc/chemin`. ### pNumber(name) > A number using `parseFloat(x)` ```ts const chemin = chemin(pNumber("myNum")); matchExact(chemin, "/3.1415"); // { myNum: 3.1415 } ``` **NOTE**: Because it uses `parseFloat` this will also accept `Infinity`, `10e2`... ### pInteger(name, options?) > A integer using `parseInt(x, 10)` ```ts 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). ```ts const chemin = chemin(pInteger("myInt", { strict: false })); matchExact(chemin, "/42fooo"); // { myInt: 42 } ``` ### pString(name) > Any non-empty string ```ts const chemin = chemin(pString("myStr")); matchExact(chemin, "/cat"); // { myStr: 'cat' } ``` ### pConstant(name) > A constant string ```ts const chemin = chemin(pConstant("edit")); matchExact(chemin, "/edit"); // {} matchExact(chemin, "/"); // false ``` ### pOptional(param) > Make any `Param` optional ```ts const chemin = chemin(pOptional(pInteger("myInt"))); matchExact(chemin, "/42"); // { myInt: { present: true, value: 42 } } matchExact(chemin, "/"); // { myInt: { present: false } } ``` ### pOptionalConst(name, path?) > An optional contant string ```ts 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. ```ts const chemin = chemin(pOptionalConst("edit")); matchExact(chemin, "/edit"); // { edit: true } matchExact(chemin, "/"); // { edit: false } ``` ### pOptionalString(name) > An optional string parameter ```ts const chemin = chemin(pOptionalString("name")); matchExact(chemin, "/paul"); // { name: 'paul' } matchExact(chemin, "/"); // { name: false } ``` ### pMultiple(param, atLeastOne?) > Allow a params to be repeated any number of time ```ts const chemin = chemin(pMultiple(pString("categories"))); matchExact(chemin, "/"); // { categories: [] } matchExact(chemin, "/foo/bar"); // { categories: ['foo', 'bar'] } ``` ```ts const chemin = chemin(pMultiple(pString("categories"), true)); matchExact(chemin, "/"); // false because atLeastOne is true matchExact(chemin, "/foo/bar"); // { categories: ['foo', 'bar'] } ``` ## Custom `Param` You can create your own `Param` to better fit your application while keeping full type-safety ! ```ts import { chemin, type TCheminParam } from "@dldc/chemin"; // match only string of 4 char [a-z0-9] function pFourCharStringId(name: N): TCheminParam { 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](https://github.com/dldc-packages/chemin/blob/main/tests/custom-advanced.test.ts). > and > [the build-in Params](https://github.com/dldc-packages/chemin/blob/main/src/params.ts). ## API ### chemin(...parts) > Create a `Chemin` Accepts any number or arguments of type `string`, `TCheminParam` or `IChemin`. **Note**: strings are converted to `pConstant`. ```ts 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. ### isChemin(maybe) > Test wether an object is a `Chemin` or not Accepts one argument and return `true` if it's a `Chemin`, false otherwise. ```ts isChemin(chemin("admin")); // true ``` ### cheminFactory(defaultSerializeOptions) 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 begining - `trailingSlash` (default: `false`): Add a slash at the end ### match(chemin, pathname) > 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 properties - `rest`: an array of string of the remaining parts of the pathname once the matching is done - `exact`: a boolean indicating if the match is exact or not (if `rest` 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. ```ts 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 ``` ### matchExact(chemin pathname) 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. ### serialize(chemin, params?, options?) > 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 begining - `trailingSlash` (default: `false`): Add a slash at the end ```ts const chemin = chemin("admin", pNumber("userId"), pOptionalConst("edit")); serialize(chemin, { userId: 42, edit: true }); // /admin/42/edit ``` ### splitPathname(pathname) > Split a pathname and prevent empty parts Accepts a string and returns an array of strings. ```ts splitPathname("/admin/user/5"); // ['admin', 'user', '5'] ``` ### partialMatch(chemin, match, part) > This function let you extract the params of a chemin that is part of another > one ```ts 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). ### matchAll(chemins, pathname) > Given an object of `Chemin` and a `pathname` return an new object with the > result of `match` for each keys ```ts 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" } }, }); ``` ### matchAllNested(chemins, pathname) > Same as `matchAll` but also match nested objects ### extract(chemin) > Return an array of all the `Chemin` it contains (as well as the `Chemin` > itself). ```ts 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` ### stringify(chemin, options) > Return a string representation of the chemin. ```ts 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 begining - `trailingSlash` (default: `false`): Add a slash at the end ### matchFirst(chemins, pathname) ### matchFirstExact(chemins, pathname) ### namespace(base, chemins) ### prefix(prefix, chemins)