{"id":20197095,"url":"https://github.com/dldc-packages/chemin","last_synced_at":"2025-12-11T21:11:16.533Z","repository":{"id":35170724,"uuid":"216521197","full_name":"dldc-packages/chemin","owner":"dldc-packages","description":"🥾 A type-safe pattern builder \u0026 route matching library written in TypeScript","archived":false,"fork":false,"pushed_at":"2025-03-29T16:27:35.000Z","size":1572,"stargazers_count":45,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-22T20:57:35.629Z","etag":null,"topics":["matching","pathname","pattern","pattern-matching","typescript","url"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dldc-packages.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["etienne-dldc"]}},"created_at":"2019-10-21T08:51:39.000Z","updated_at":"2025-10-19T20:49:49.000Z","dependencies_parsed_at":"2023-02-18T03:45:48.727Z","dependency_job_id":"915a9ae3-05c4-4315-a86d-8ea8c810bed2","html_url":"https://github.com/dldc-packages/chemin","commit_stats":null,"previous_names":["etienne-dldc/chemin"],"tags_count":50,"template":false,"template_full_name":null,"purl":"pkg:github/dldc-packages/chemin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dldc-packages%2Fchemin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dldc-packages%2Fchemin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dldc-packages%2Fchemin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dldc-packages%2Fchemin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dldc-packages","download_url":"https://codeload.github.com/dldc-packages/chemin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dldc-packages%2Fchemin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27670295,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["matching","pathname","pattern","pattern-matching","typescript","url"],"created_at":"2024-11-14T04:27:19.734Z","updated_at":"2025-12-11T21:11:16.512Z","avatar_url":"https://github.com/dldc-packages.png","language":"TypeScript","funding_links":["https://github.com/sponsors/etienne-dldc"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://github.com/dldc-packages/chemin/raw/main/design/logo.png\" width=\"900\" alt=\"chemin logo\"\u003e\n\u003c/p\u003e\n\n# 🥾 Chemin\n\n\u003e A type-safe pattern builder \u0026 route matching library written in TypeScript\n\n## Gist\n\n```js\nimport { chemin, pNumber, pOptionalConst } from \"@dldc/chemin\";\n\n// admin/post/:postId(number)/delete?\nconst path = chemin(\n  \"admin\",\n  \"post\",\n  pNumber(\"postId\"),\n  pOptionalConst(\"delete\"),\n);\n\nconsole.log(path.match(\"/no/valid\"));\n// =\u003e null\n\nconst match = path.match(\"/admin/post/45\");\nconsole.log(match);\n// =\u003e { rest: [], exact: true, params: { postId: 45, delete: false } }\n// match.params is typed as { postId: number, delete: boolean } !\n```\n\n## Composition\n\nYou can use a `Chemin` inside another one to easily compose your routes !\n\n```ts\nimport { chemin, pNumber, pString } from \"@dldc/chemin\";\n\nconst postFragment = chemin(\"post\", pNumber(\"postId\"));\nconst postAdmin = chemin(\"admin\", pString(\"userId\"), postFragment, \"edit\");\n\nconsole.log(postAdmin.stringify()); // /admin/:userId/post/:postId(number)/edit\n```\n\n## Build-in params\n\nThe following params are build-in and exported from `@dldc/chemin`.\n\n### pNumber(name)\n\n\u003e A number using `parseFloat(x)`\n\n```ts\nconst chemin = chemin(pNumber(\"myNum\"));\nmatchExact(chemin, \"/3.1415\"); // { myNum: 3.1415 }\n```\n\n**NOTE**: Because it uses `parseFloat` this will also accept `Infinity`,\n`10e2`...\n\n### pInteger(name, options?)\n\n\u003e A integer using `parseInt(x, 10)`\n\n```ts\nconst chemin = chemin(pInteger(\"myInt\"));\nmatchExact(chemin, \"/42\"); // { myInt: 42 }\n```\n\nThe `options` parameter is optional and accepts a `strict` boolean property\n(`true` by default). When strict is set to `true` (the default) it will only\nmatch if the parsed number is the same as the raw value (so `1.0` or `42blabla`\nwill not match).\n\n```ts\nconst chemin = chemin(pInteger(\"myInt\", { strict: false }));\nmatchExact(chemin, \"/42fooo\"); // { myInt: 42 }\n```\n\n### pString(name)\n\n\u003e Any non-empty string\n\n```ts\nconst chemin = chemin(pString(\"myStr\"));\nmatchExact(chemin, \"/cat\"); // { myStr: 'cat' }\n```\n\n### pConstant(name)\n\n\u003e A constant string\n\n```ts\nconst chemin = chemin(pConstant(\"edit\"));\nmatchExact(chemin, \"/edit\"); // {}\nmatchExact(chemin, \"/\"); // false\n```\n\n### pOptional(param)\n\n\u003e Make any `Param` optional\n\n```ts\nconst chemin = chemin(pOptional(pInteger(\"myInt\")));\nmatchExact(chemin, \"/42\"); // { myInt: { present: true, value: 42 } }\nmatchExact(chemin, \"/\"); // { myInt: { present: false } }\n```\n\n### pOptionalConst(name, path?)\n\n\u003e An optional contant string\n\n```ts\nconst chemin = chemin(pOptionalConst(\"isEditing\", \"edit\"));\nmatchExact(chemin, \"/edit\"); // { isEditing: true }\nmatchExact(chemin, \"/\"); // { isEditing: false }\n```\n\nIf `path` is omitted then the name is used as the path.\n\n```ts\nconst chemin = chemin(pOptionalConst(\"edit\"));\nmatchExact(chemin, \"/edit\"); // { edit: true }\nmatchExact(chemin, \"/\"); // { edit: false }\n```\n\n### pOptionalString(name)\n\n\u003e An optional string parameter\n\n```ts\nconst chemin = chemin(pOptionalString(\"name\"));\nmatchExact(chemin, \"/paul\"); // { name: 'paul' }\nmatchExact(chemin, \"/\"); // { name: false }\n```\n\n### pMultiple(param, atLeastOne?)\n\n\u003e Allow a params to be repeated any number of time\n\n```ts\nconst chemin = chemin(pMultiple(pString(\"categories\")));\nmatchExact(chemin, \"/\"); // { categories: [] }\nmatchExact(chemin, \"/foo/bar\"); // { categories: ['foo', 'bar'] }\n```\n\n```ts\nconst chemin = chemin(pMultiple(pString(\"categories\"), true));\nmatchExact(chemin, \"/\"); // false because atLeastOne is true\nmatchExact(chemin, \"/foo/bar\"); // { categories: ['foo', 'bar'] }\n```\n\n## Custom `Param`\n\nYou can create your own `Param` to better fit your application while keeping\nfull type-safety !\n\n```ts\nimport { chemin, type TCheminParam } from \"@dldc/chemin\";\n\n// match only string of 4 char [a-z0-9]\nfunction pFourCharStringId\u003cN extends string\u003e(name: N): TCheminParam\u003cN, string\u003e {\n  const reg = /^[a-z0-9]{4}$/;\n  return {\n    factory: pFourCharStringId,\n    name,\n    meta: null,\n    isEqual: (other) =\u003e other.name === name,\n    match: (...all) =\u003e {\n      if (all[0].match(reg)) {\n        return { match: true, value: all[0], next: all.slice(1) };\n      }\n      return { match: false, next: all };\n    },\n    serialize: (value) =\u003e value,\n    stringify: () =\u003e `:${name}(id4)`,\n  };\n}\n\nconst path = chemin(\"item\", pFourCharStringId(\"itemId\"));\nconsole.log(path.match(\"/item/a4e3t\")); // null (5 char)\nconsole.log(path.match(\"/item/A4e3\")); // null (because A is uppercase)\nconsole.log(path.match(\"/item/a4e3\")); // { rest: [], exact: true, params: { itemId: 'a4e3' } }\n```\n\n\u003e Take a look a\n\u003e [the custom-advanced.test.ts example](https://github.com/dldc-packages/chemin/blob/main/tests/custom-advanced.test.ts).\n\u003e and\n\u003e [the build-in Params](https://github.com/dldc-packages/chemin/blob/main/src/params.ts).\n\n## API\n\n### chemin(...parts)\n\n\u003e Create a `Chemin`\n\nAccepts any number or arguments of type `string`, `TCheminParam` or `TChemin`.\n\n**Note**: strings are converted to `pConstant`.\n\n```ts\nchemin(\"admin\", pNumber(\"userId\"), pOptionalConst(\"edit\"));\n```\n\nThe `chemin` function returns an object with the following properties:\n\n- `parts`: an array of the parts (other `Chemin`s or `Param`s), this is what was\n  passed to the `chemin` function except that strings are converted to\n  `pConstant`.\n- `match(pathname)`: test a chemin against a pathname, see `match` for more\n  details.\n- `matchExact(pathname)`: test a chemin against a pathname for an exact match,\n  see `matchExact` for more details.\n- `stringify(params?, options?)`: serialize a chemin, see `stringify` for more\n  details.\n- `serialize(params?, options?)`: serialize a chemin, see `serialize` for more\n  details.\n- `extract()`: return an array of all the `Chemin` it contains (as well as the\n  `Chemin` itself), see `extract` for more details.\n- `flatten()`: return all the `Param` it contains, see `flatten` for more\n  details.\n\n_Note_: Most of these functions are also exported as standalone functions (see\nbelow). The only difference is that `extract` and `flatten` are cached when\ncalled on a `Chemin` itself, but you should rarely need to use them anyway.\n\n### isChemin(maybe)\n\n\u003e Test wether an object is a `Chemin` or not\n\nAccepts one argument and return `true` if it's a `Chemin`, false otherwise.\n\n```ts\nisChemin(chemin(\"admin\")); // true\n```\n\n### cheminFactory(defaultSerializeOptions)\n\nThe `cheminFactory` function returns a function that works exactly like `chemin`\nbut with a default `serialize` / `stringify` options.\n\nThe `defaultSerializeOptions` parameter is optional and accepts two `boolean`\nproperties:\n\n- `leadingSlash` (default `true`): Add a slash at the begining\n- `trailingSlash` (default: `false`): Add a slash at the end\n\n### match(chemin, pathname)\n\n\u003e Test a chemin against a pathname\n\nReturns `null` or `TCheminMatch`.\n\n- `pathname` can be either a string (`/admin/user/5`) or an array of strings\n  (`['admin', 'user', '5']`)\n- `TCheminMatch` is an object with three properties\n  - `rest`: an array of string of the remaining parts of the pathname once the\n    matching is done\n  - `exact`: a boolean indicating if the match is exact or not (if `rest` is\n    empty or not)\n  - `params`: an object of params extracted from the matching\n\n**Note**: When `pathname` is a `string`, it is splitted using the\n`splitPathname` function. This function is exported so you can use it to split\nyour pathnames in the same way.\n\n```ts\nimport { chemin, match, pNumber, pOptionalConst } from \"@dldc/chemin\";\n\nconst chemin = chemin(\"admin\", pNumber(\"userId\"), pOptionalConst(\"edit\"));\nmatch(chemin, \"/admin/42/edit\"); // { rest: [], exact: true, params: { userId: 42, edit: true } }\nmatch(chemin, \"/admin/42/edit/rest\"); // { rest: ['rest'], exact: false, params: { userId: 42, edit: true } }\nmatch(chemin, \"/noop\"); // null\n```\n\n### matchExact(chemin pathname)\n\nAccepts the same arguments as `match` but return `null` if the path does not\nmatch or if `rest` is not empty, otherwise it returns the `params` object\ndirectly.\n\n### serialize(chemin, params?, options?)\n\n\u003e Print a chemin from its params.\n\nAccepts a `chemin` some `params` (an object or `null`) and an optional `option`\nobject.\n\nThe option object accepts two `boolean` properties:\n\n- `leadingSlash` (default `true`): Add a slash at the begining\n- `trailingSlash` (default: `false`): Add a slash at the end\n\n```ts\nconst chemin = chemin(\"admin\", pNumber(\"userId\"), pOptionalConst(\"edit\"));\nserialize(chemin, { userId: 42, edit: true }); // /admin/42/edit\n```\n\n### splitPathname(pathname)\n\n\u003e Split a pathname and prevent empty parts\n\nAccepts a string and returns an array of strings.\n\n```ts\nsplitPathname(\"/admin/user/5\"); // ['admin', 'user', '5']\n```\n\n### partialMatch(chemin, match, part)\n\n\u003e This function let you extract the params of a chemin that is part of another\n\u003e one\n\n```ts\nconst workspaceBase = chemin(\"workspace\", pString(\"tenant\"));\n\nconst routes = [\n  chemin(\"home\"), // home\n  chemin(\"settings\"), // settings\n  chemin(workspaceBase, \"home\"), // workspace home\n  chemin(workspaceBase, \"settings\"), // workspace settings\n];\n\nfunction app(pathname: string) {\n  const route = matchFirst(routes, pathname);\n  if (!route) {\n    return { route: null };\n  }\n  const { chemin, match } = route;\n  // extract the tenant from the workspace if it's a workspace route\n  const params = partialMatch(chemin, match, workspaceBase);\n  // params is typed as { tenant: string } | null\n  if (params) {\n    return { tenant: params.tenant, route: chemin.stringify() };\n  }\n  return { route: chemin.stringify() };\n}\n```\n\n**Note**: This is based on reference equality so it will not work if you create\na new `Chemin` with the same parts: `chemin('workspace', pString('tenant'))` !\n\n**Note 2**: In reality this function simply returns the `match.params` object if\nthe `part` is contained in `chemin` or `null` otherwise. This mean that you\nmight get more properties that what the type gives you (but this is quite\ncommoin in TypeScript).\n\n### matchAll(chemins, pathname)\n\n\u003e Given an object of `Chemin` and a `pathname` return an new object with the\n\u003e result of `match` for each keys\n\n```ts\nconst chemins = {\n  home: chemin(\"home\"),\n  workspace: chemin(\"workspace\", pString(\"tenant\")),\n  workspaceSettings: chemin(\"workspace\", pString(\"tenant\"), \"settings\"),\n};\n\nconst match = matchAll(chemins, \"/workspace/123/settings\");\nexpect(match).toEqual({\n  home: null,\n  workspace: { rest: [\"settings\"], exact: false, params: { tenant: \"123\" } },\n  workspaceSettings: { rest: [], exact: true, params: { tenant: \"123\" } },\n});\n```\n\n### matchAllNested(chemins, pathname)\n\n\u003e Same as `matchAll` but also match nested objects\n\n### extract(chemin)\n\n\u003e Return an array of all the `Chemin` it contains (as well as the `Chemin`\n\u003e itself).\n\n```ts\nimport { Chemin } from \"@dldc/chemin\";\n\nconst admin = chemin(\"admin\");\nconst adminUser = chemin(admin, \"user\");\n\nadminUser.extract(); // [adminUser, admin];\n```\n\n**Note**: You probably don't need this but it's used internally in\n`partialMatch`\n\n### stringify(chemin, options)\n\n\u003e Return a string representation of the chemin.\n\n```ts\nimport { Chemin, pNumber, pString, stringify } from \"@dldc/chemin\";\n\nconst postFragment = chemin(\"post\", pNumber(\"postId\"));\nconst postAdmin = chemin(\"admin\", pString(\"userId\"), postFragment, \"edit\");\n\nconsole.log(stringify(postAdmin)); // /admin/:userId/post/:postId(number)/edit\n```\n\nThe option object accepts two `boolean` properties:\n\n- `leadingSlash` (default `true`): Add a slash at the begining\n- `trailingSlash` (default: `false`): Add a slash at the end\n\n### matchFirst(chemins, pathname)\n\n### matchFirstExact(chemins, pathname)\n\n### namespace(base, chemins)\n\n### prefix(prefix, chemins)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdldc-packages%2Fchemin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdldc-packages%2Fchemin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdldc-packages%2Fchemin/lists"}