import {
MatcherPatternPathStatic,
MatcherPatternPathStar,
+ MatcherPatternPathCustom,
} from './matcher-pattern'
+import { pathEncoded } from '../resolver-abstract'
+import { invalid } from './errors'
describe('MatcherPatternPathStatic', () => {
describe('match()', () => {
})
})
})
+
+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')
+ })
+})
// 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<TIn>) => TOut
set?: (value: NoInfer<TOut>) => TIn
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
: 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<TIn, TOut>
+}
+
+export class MatcherPatternPathCustom implements MatcherPatternPath {
+ // private paramsKeys: string[]
+
+ constructor(
+ readonly re: RegExp,
+ readonly params: Record<string, MatcherPatternPathDynamicParam>,
+ 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<string | number>
+ ) {
+ // 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.