@dldc/chemin
v11.0.4
Published
A type-safe pattern builder & route matching library written in TypeScript
Downloads
7
Maintainers
Readme
🥾 Chemin
A type-safe pattern builder & route matching library written in TypeScript
Gist
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 !
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)
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)
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 }
pString(name)
Any non-empty string
const chemin = chemin(pString('myStr'));
matchExact(chemin, '/cat'); // { myStr: 'cat' }
pConstant(name)
A constant string
const chemin = chemin(pConstant('edit'));
matchExact(chemin, '/edit'); // {}
matchExact(chemin, '/'); // false
pOptional(param)
Make any
Param
optional
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
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 }
pOptionalString(name)
An optional string parameter
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
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'] }
Custom 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.
API
chemin(...parts)
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.
isChemin(maybe)
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
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
(defaulttrue
): Add a slash at the beginingtrailingSlash
(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 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, pNumber, pOptionalConst, match } 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
(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
splitPathname(pathname)
Split a pathname and prevent empty parts
Accepts a string and returns an array of strings.
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
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 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' } },
});
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 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
stringify(chemin, options)
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