From: Eduardo San Martin Morote Date: Mon, 4 Aug 2025 10:00:00 +0000 (+0200) Subject: feat: MatcherPatternPathCustom X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=5fa416a7a0e1b06697c5c875e4a434c396cdd8d8;p=thirdparty%2Fvuejs%2Frouter.git feat: MatcherPatternPathCustom --- diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts index c3f8f021..df6bf36d 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts @@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest' import { MatcherPatternPathStatic, MatcherPatternPathStar, + MatcherPatternPathCustom, } from './matcher-pattern' +import { pathEncoded } from '../resolver-abstract' +import { invalid } from './errors' describe('MatcherPatternPathStatic', () => { describe('match()', () => { @@ -100,3 +103,126 @@ describe('MatcherPatternPathStar', () => { }) }) }) + +describe('MatcherPatternPathCustom', () => { + it('single param', () => { + const pattern = new MatcherPatternPathCustom( + /^\/teams\/([^/]+?)\/b$/i, + { + // all defaults + teamId: {}, + }, + ({ teamId }) => { + if (typeof teamId !== 'string') { + throw invalid('teamId must be a string') + } + return pathEncoded`/teams/${teamId}/b` + } + ) + + expect(pattern.match('/teams/123/b')).toEqual({ + teamId: '123', + }) + expect(pattern.match('/teams/abc/b')).toEqual({ + teamId: 'abc', + }) + expect(() => pattern.match('/teams/123/c')).toThrow() + expect(() => pattern.match('/teams/123/b/c')).toThrow() + expect(() => pattern.match('/teams')).toThrow() + expect(() => pattern.match('/teams/')).toThrow() + }) + + it('decodes single param', () => { + const pattern = new MatcherPatternPathCustom( + /^\/teams\/([^/]+?)$/i, + { + teamId: {}, + }, + ({ teamId }) => { + if (typeof teamId !== 'string') { + throw invalid('teamId must be a string') + } + return pathEncoded`/teams/${teamId}` + } + ) + expect(pattern.match('/teams/a%20b')).toEqual({ teamId: 'a b' }) + expect(pattern.build({ teamId: 'a b' })).toBe('/teams/a%20b') + }) + + it('optional param', () => { + const pattern = new MatcherPatternPathCustom( + /^\/teams(?:\/([^/]+?))?\/b$/i, + { + teamId: { optional: true }, + }, + ({ teamId }) => { + if (teamId != null && typeof teamId !== 'string') { + throw invalid('teamId must be a string') + } + return teamId ? pathEncoded`/teams/${teamId}/b` : '/teams/b' + } + ) + + expect(pattern.match('/teams/b')).toEqual({ teamId: null }) + expect(pattern.match('/teams/123/b')).toEqual({ teamId: '123' }) + expect(() => pattern.match('/teams/123/c')).toThrow() + expect(() => pattern.match('/teams/123/b/c')).toThrow() + expect(pattern.build({ teamId: '123' })).toBe('/teams/123/b') + expect(pattern.build({ teamId: null })).toBe('/teams/b') + }) + + it('repeatable param', () => { + const pattern = new MatcherPatternPathCustom( + /^\/teams\/(.+?)\/b$/i, + { + teamId: { repeat: true }, + }, + ({ teamId }) => { + if (!Array.isArray(teamId)) { + throw invalid('teamId must be an array') + } + return '/teams/' + teamId.join('/') + '/b' + } + ) + + expect(pattern.match('/teams/123/b')).toEqual({ teamId: ['123'] }) + expect(pattern.match('/teams/123/456/b')).toEqual({ + teamId: ['123', '456'], + }) + expect(() => pattern.match('/teams/123/c')).toThrow() + expect(() => pattern.match('/teams/123/b/c')).toThrow() + expect(pattern.build({ teamId: ['123'] })).toBe('/teams/123/b') + expect(pattern.build({ teamId: ['123', '456'] })).toBe('/teams/123/456/b') + }) + + it('repeatable optional param', () => { + const pattern = new MatcherPatternPathCustom( + /^\/teams(?:\/(.+?))?\/b$/i, + { + teamId: { repeat: true, optional: true }, + }, + ({ teamId }) => { + if (!Array.isArray(teamId)) { + throw invalid('teamId must be an array') + } + const joined = teamId.join('/') + return teamId + ? '/teams' + (joined ? '/' + joined : '') + '/b' + : '/teams/b' + } + ) + + expect(pattern.match('/teams/123/b')).toEqual({ teamId: ['123'] }) + expect(pattern.match('/teams/123/456/b')).toEqual({ + teamId: ['123', '456'], + }) + expect(pattern.match('/teams/b')).toEqual({ teamId: [] }) + + expect(() => pattern.match('/teams/123/c')).toThrow() + expect(() => pattern.match('/teams/123/b/c')).toThrow() + + expect(pattern.build({ teamId: ['123'] })).toBe('/teams/123/b') + expect(pattern.build({ teamId: ['123', '456'] })).toBe('/teams/123/456/b') + expect(pattern.build({ teamId: [] })).toBe('/teams/b') + }) +}) diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts index f939aa57..4f92a333 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts @@ -119,8 +119,12 @@ export class MatcherPatternPathStar // new MatcherPatternPathStatic('/team') export interface Param_GetSet< - TIn extends string | string[] = string | string[], - TOut = TIn, + TIn extends string | string[] | null | undefined = + | string + | string[] + | null + | undefined, + TOut = string | string[] | null, > { get?: (value: NoInfer) => TOut set?: (value: NoInfer) => TIn @@ -145,7 +149,13 @@ export function defineParamParser(parser: { return parser } -const PATH_PARAM_DEFAULT_GET = (value: string | string[]) => value +interface IdFn { + (v: undefined | null): null + (v: string): string + (v: string[]): string[] +} + +const PATH_PARAM_DEFAULT_GET = (value => value ?? null) as IdFn const PATH_PARAM_DEFAULT_SET = (value: unknown) => value && Array.isArray(value) ? value.map(String) : String(value) // TODO: `(value an null | undefined)` for types @@ -183,6 +193,79 @@ export type ParamsFromParsers

> = { : never } +/** + * TODO: it should accept a dict of param parsers for each param and if they are repeatable and optional + * The object order matters, they get matched in that order + */ + +interface MatcherPatternPathDynamicParam< + TIn extends string | string[] | null | undefined = + | string + | string[] + | null + | undefined, + TOut = string | string[] | null, +> { + repeat?: boolean + optional?: boolean + parser?: Param_GetSet +} + +export class MatcherPatternPathCustom implements MatcherPatternPath { + // private paramsKeys: string[] + + constructor( + readonly re: RegExp, + readonly params: Record, + readonly build: (params: MatcherParamsFormatted) => string + // A better version could be using all the parts to join them + // .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456 + // numbers are indexes of the params in the params object keys + // readonly pathParts: Array + ) { + // this.paramsKeys = Object.keys(this.params) + } + + match(path: string): MatcherParamsFormatted { + const match = path.match(this.re) + if (!match) { + throw miss() + } + const params = {} as MatcherParamsFormatted + let i = 1 // index in match array + for (const paramName in this.params) { + const currentParam = this.params[paramName] + // an optional group in the regexp will return undefined + const currentMatch = match[i++] as string | undefined + if (__DEV__ && !currentParam.optional && !currentMatch) { + warn( + `Unexpected undefined value for param "${paramName}". Regexp: ${String(this.re)}. path: "${path}". This is likely a bug.` + ) + throw miss() + } + + const value = currentParam.repeat + ? (currentMatch?.split('/') || []).map( + // using just decode makes the type inference fail + v => decode(v) + ) + : decode(currentMatch) + + console.log(paramName, currentParam, value) + + params[paramName] = (currentParam.parser?.get || (v => v ?? null))(value) + } + + if (__DEV__ && i !== match.length) { + warn( + `Regexp matched ${match.length} params, but ${i} params are defined. Found when matching "${path}" against ${String(this.re)}` + ) + } + + return params + } +} + /** * Matcher for dynamic paths, e.g. `/team/:id/:name`. * Supports one, one or zero, one or more and zero or more params. diff --git a/packages/router/src/experimental/route-resolver/resolver-abstract.ts b/packages/router/src/experimental/route-resolver/resolver-abstract.ts index d475590b..abc70e91 100644 --- a/packages/router/src/experimental/route-resolver/resolver-abstract.ts +++ b/packages/router/src/experimental/route-resolver/resolver-abstract.ts @@ -228,7 +228,7 @@ export function pathEncoded( ? params[i].map(encodeParam).join('/') : encodeParam(params[i])) ) - }) + }, '') } export interface ResolverLocationAsNamed { name: RecordName