From 4c69c9c398932784ae8d0b826f7befb44aefb8eb Mon Sep 17 00:00:00 2001 From: James Forbes Date: Sun, 11 Feb 2024 07:02:26 +0000 Subject: [PATCH] Normalizing rest / paths Fix propagation of rest in toPath --- lib/fromPath.ts | 3 ++- lib/index.ts | 11 +++++----- lib/normalize.ts | 25 +++++++++++++++++++++++ test/fromPath.ts | 24 +++++++++++----------- test/index.ts | 52 +++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 lib/normalize.ts diff --git a/lib/fromPath.ts b/lib/fromPath.ts index 97b6576..ccd1156 100644 --- a/lib/fromPath.ts +++ b/lib/fromPath.ts @@ -1,4 +1,5 @@ import { Either } from "./either.js"; +import { normalizeRest } from "./normalize.js"; type Mode = | "initialize" @@ -200,7 +201,7 @@ export function safe( if (rest) { score = Math.max(0, score - 1); } - return { type: "Either", tag: "Right", value: { rest, value, score } }; + return { type: "Either", tag: "Right", value: { rest: normalizeRest(rest), value, score } }; } export function unsafe( _path: string, _pattern: string): { rest: string, value: Record, score: number } { diff --git a/lib/index.ts b/lib/index.ts index d274837..f24ab25 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,6 +2,7 @@ import { Either } from "./either.js"; import * as ParsePath from "./fromPath.js"; +import { normalizePathSegment, normalizeRest } from "./normalize.js"; export type Patterns = string | string[]; @@ -88,7 +89,6 @@ function otherwise(tags:string[]) { ) } - export function type( type: N, routes: D @@ -134,7 +134,8 @@ export function type( } if (bestPath) { - return { type: "Either", tag: "Right", value: bestPath.join("/") }; + const value = normalizePathSegment(bestPath.concat(route.value.rest).join("/")) + return { type: "Either", tag: "Right", value }; } else if (errors.length) { return { type: "Either", tag: "Left", value: new Error(errors[0]) }; } else { @@ -178,7 +179,7 @@ export function type( } } else { if (bestRoute == null || result.value.score > bestRank) { - bestRoute = { type, tag, value: { ...result.value.value, rest: result.value.rest } }; + bestRoute = api[tag]({ ...result.value.value, rest: result.value.rest }) bestRank = result.value.score; } } @@ -230,9 +231,7 @@ export function type( for (const [tag, of] of Object.entries(routes)) { api[tag] = (value:any = {}) => { - if ( !value.rest ) { - value.rest = '/' - } + value.rest = normalizeRest(value.rest) return { type, tag, value } }; diff --git a/lib/normalize.ts b/lib/normalize.ts new file mode 100644 index 0000000..826beb2 --- /dev/null +++ b/lib/normalize.ts @@ -0,0 +1,25 @@ + +export function normalizePathSegment(s: string): string{ + if( s == '' || s == '/' ) { + return '/' + } else if ( s.startsWith('/') && s.endsWith('/') ) { + return s.slice(0, -1) + } else if ( !s.startsWith('/') && s.endsWith('/') ) { + return '/' + s.slice(0, -1) + } else { + return s + } +} + +export function normalizeRest(s:string): string { + if ( !s ) { + s = '' + } + if (s.at(0) == '/') { + s = s.slice(1) + } + if (s.at(-1) == '/') { + s = s.slice(0,-1) + } + return s +} \ No newline at end of file diff --git a/test/fromPath.ts b/test/fromPath.ts index 4427c16..076d09f 100644 --- a/test/fromPath.ts +++ b/test/fromPath.ts @@ -22,7 +22,7 @@ test("fromPath: simple", () => { .sort((a, b) => b.value.score - a.value.score) .map((x) => x.value)[0]; - assert(best.rest == "/", "most specific wins"); + assert(best.rest == "", "most specific wins"); assert.deepEqual(best.value, { nam: "james" }, "Expected parsed URL"); }); @@ -45,8 +45,8 @@ test("fromPath: rest", () => { .sort((a, b) => b.value.score - a.value.score) .map((x) => x.value); - assert(sorted[0].rest == "/", "most specific wins"); - assert(sorted[1].rest == "/extra", "rest has expected value"); + assert.equal(sorted[0].rest, "", "most specific wins"); + assert.equal(sorted[1].rest, "extra", "rest has expected value"); assert.deepEqual( sorted[1].value, { name: "james" }, @@ -58,34 +58,34 @@ test("fromPath: garbage", () => { assert.deepEqual(safe("", ""), { type: "Either", tag: "Right", - value: { rest: "/", value: {}, score: 0 }, + value: { rest: "", value: {}, score: 0 }, }); assert.deepEqual(safe("////", ""), { type: "Either", tag: "Right", - value: { rest: "/", value: {}, score: 0 }, + value: { rest: "", value: {}, score: 0 }, }); assert.deepEqual(safe("", "////////"), { type: "Either", tag: "Right", - value: { rest: "/", value: {}, score: 0 }, + value: { rest: "", value: {}, score: 0 }, }); assert.deepEqual(safe("///////", "////////"), { type: "Either", tag: "Right", - value: { rest: "/", value: {}, score: 0 }, + value: { rest: "", value: {}, score: 0 }, }); assert.deepEqual(safe("a", ":a"), { type: "Either", tag: "Right", - value: { rest: "/", value: { a: "a" }, score: 1 }, + value: { rest: "", value: { a: "a" }, score: 1 }, }); assert.deepEqual(safe(":::::", ":a"), { type: "Either", tag: "Right", - value: { rest: "/", value: { a: ":::::" }, score: 1 }, + value: { rest: "", value: { a: ":::::" }, score: 1 }, }); // @ts-expect-error @@ -124,10 +124,10 @@ test("fromPath: complex", () => { assert.match(fail.message, /:c/); } assert.deepEqual(success, [ - { rest: "/a", value: {}, score: 0 }, - { rest: "/", value: {}, score: 3 }, + { rest: "a", value: {}, score: 0 }, + { rest: "", value: {}, score: 3 }, { - rest: "/and/something/extra", + rest: "and/something/extra", value: { a: "welcome", b: "james", c: "you", g: "cool" }, score: 19, }, diff --git a/test/index.ts b/test/index.ts index ca9d8b6..069e357 100644 --- a/test/index.ts +++ b/test/index.ts @@ -12,12 +12,12 @@ test("index: basic", () => { assert.deepEqual(Example.A({ a_id: "hello" }), { type: "Example", tag: "A", - value: { a_id: "hello", rest: "/" }, + value: { a_id: "hello", rest: "" }, }); assert.deepEqual(Example.B({ b_id: "hello" }), { type: "Example", tag: "B", - value: { b_id: "hello", rest: "/" }, + value: { b_id: "hello", rest: "" }, }); assert.equal(Example.definition.A({ a_id: "" }), "/a/:a_id"); @@ -72,7 +72,7 @@ test("index: fromPath", () => { assert.throws(() => Example.fromPath("/"), /Expected literal/); assert.deepEqual( - Example.C({ c_id: "cool", rest: "/" }), + Example.C({ c_id: "cool", rest: "" }), Example.fromPath("/c/cool") ); assert.deepEqual( @@ -148,3 +148,49 @@ test("index: matchOr", () => { assert.deepEqual(res, Example.A({ a_id: "notcool" })); } }); + +test('index: rest', () => { + const Odin = superouter.type('Odin', { + Home: (_: Record) => '/', + Organization: (_: Record) => '/admin/organizations' + }) + + assert.equal(Odin.toPath(Odin.Organization({ rest: '1' })), '/admin/organizations/1') + assert.equal(Odin.toPath(Odin.Organization({ rest: '/1' })), '/admin/organizations/1') + assert.equal(Odin.toPath(Odin.Organization({ rest: '/1/' })), '/admin/organizations/1') + assert.equal(Odin.toPath(Odin.Organization({ rest: '1/' })), '/admin/organizations/1') + + const Orgs = superouter.type('Orgs', { + List: (_: { organization_id: string }) => `/:organization_id`, + Group: (_: { organization_id: string, group_id: string }) => `/:organization_id/groups/:group_id` + }) + + { + const parentPath = Odin.toPath(Odin.Organization({ rest: '1' })) + + const child = Orgs.fromPath(parentPath.replace(`/admin/organizations`, '')) + + assert.equal(Orgs.toPath(child), '/1') + } + { + const originalUrl = `/admin/organizations/1/groups/2` + + const originalRoute = Odin.fromPath(originalUrl) + + assert.deepEqual(originalRoute, Odin.Organization({ rest: '1/groups/2' })) + + const parentPath = Odin.toPath(originalRoute) + + assert.equal(originalUrl, parentPath) + + const prefix = `/admin/organizations` + + const child = Orgs.fromPath(parentPath.replace(prefix, '')) + + assert.equal(Orgs.toPath(child), '/1/groups/2') + + assert.equal(prefix + Orgs.toPath(child), originalUrl) + + + } +}) \ No newline at end of file