1- import { MatcherName , MatcherQueryParams } from './matcher'
1+ import { decode , MatcherName , MatcherQueryParams } from './matcher'
22import { EmptyParams , MatcherParamsFormatted } from './matcher-location'
33import { miss } from './matchers/errors'
44
@@ -19,14 +19,28 @@ export interface MatcherPatternParams_Base<
1919 TIn = string ,
2020 TOut extends MatcherParamsFormatted = MatcherParamsFormatted
2121> {
22+ /**
23+ * Matches a serialized params value against the pattern.
24+ *
25+ * @param value - params value to parse
26+ * @throws {MatchMiss } if the value doesn't match
27+ * @returns parsed params
28+ */
2229 match ( value : TIn ) : TOut
30+
31+ /**
32+ * Build a serializable value from parsed params. Should apply encoding if the
33+ * returned value is a string (e.g path and hash should be encoded but query
34+ * shouldn't).
35+ *
36+ * @param value - params value to parse
37+ */
2338 build ( params : TOut ) : TIn
2439}
2540
2641export interface MatcherPatternPath <
27- TParams extends MatcherParamsFormatted = // | undefined // | void // so it might be a bit more convenient // TODO: should we allow to not return anything? It's valid to spread null and undefined
28- // | null
29- MatcherParamsFormatted
42+ // TODO: should we allow to not return anything? It's valid to spread null and undefined
43+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted // | null // | undefined // | void // so it might be a bit more convenient
3044> extends MatcherPatternParams_Base < string , TParams > { }
3145
3246export class MatcherPatternPathStatic
@@ -48,6 +62,143 @@ export class MatcherPatternPathStatic
4862// example of a static matcher built at runtime
4963// new MatcherPatternPathStatic('/')
5064
65+ export interface Param_GetSet <
66+ TIn extends string | string [ ] = string | string [ ] ,
67+ TOut = TIn
68+ > {
69+ get ?: ( value : NoInfer < TIn > ) => TOut
70+ set ?: ( value : NoInfer < TOut > ) => TIn
71+ }
72+
73+ export type ParamParser_Generic =
74+ | Param_GetSet < string , any >
75+ | Param_GetSet < string [ ] , any >
76+ // TODO: these are possible values for optional params
77+ // | null | undefined
78+
79+ /**
80+ * Type safe helper to define a param parser.
81+ *
82+ * @param parser - the parser to define. Will be returned as is.
83+ */
84+ /*! #__NO_SIDE_EFFECTS__ */
85+ export function defineParamParser < TOut , TIn extends string | string [ ] > ( parser : {
86+ get ?: ( value : TIn ) => TOut
87+ set ?: ( value : TOut ) => TIn
88+ } ) : Param_GetSet < TIn , TOut > {
89+ return parser
90+ }
91+
92+ const PATH_PARAM_DEFAULT_GET = ( value : string | string [ ] ) => value
93+ const PATH_PARAM_DEFAULT_SET = ( value : unknown ) =>
94+ value && Array . isArray ( value ) ? value . map ( String ) : String ( value )
95+ // TODO: `(value an null | undefined)` for types
96+
97+ /**
98+ * NOTE: I tried to make this generic and infer the types from the params but failed. This is what I tried:
99+ * ```ts
100+ * export type ParamsFromParsers<P extends Record<string, ParamParser_Generic>> = {
101+ * [K in keyof P]: P[K] extends Param_GetSet<infer TIn, infer TOut>
102+ * ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[]
103+ * ? TIn
104+ * : TOut
105+ * : never
106+ * }
107+ *
108+ * export class MatcherPatternPathDynamic<
109+ * ParamsParser extends Record<string, ParamParser_Generic>
110+ * > implements MatcherPatternPath<ParamsFromParsers<ParamsParser>>
111+ * {
112+ * private params: Record<string, Required<ParamParser_Generic>> = {}
113+ * constructor(
114+ * private re: RegExp,
115+ * params: ParamsParser,
116+ * public build: (params: ParamsFromParsers<ParamsParser>) => string
117+ * ) {}
118+ * ```
119+ * It ended up not working in one place or another. It could probably be fixed by
120+ */
121+
122+ export type ParamsFromParsers < P extends Record < string , ParamParser_Generic > > = {
123+ [ K in keyof P ] : P [ K ] extends Param_GetSet < infer TIn , infer TOut >
124+ ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[]
125+ ? TIn
126+ : TOut
127+ : never
128+ }
129+
130+ export class MatcherPatternPathDynamic <
131+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted
132+ > implements MatcherPatternPath < TParams >
133+ {
134+ private params : Record < string , Required < ParamParser_Generic > > = { }
135+ constructor (
136+ private re : RegExp ,
137+ params : Record < keyof TParams , ParamParser_Generic > ,
138+ public build : ( params : TParams ) => string ,
139+ private opts : { repeat ?: boolean ; optional ?: boolean } = { }
140+ ) {
141+ for ( const paramName in params ) {
142+ const param = params [ paramName ]
143+ this . params [ paramName ] = {
144+ get : param . get || PATH_PARAM_DEFAULT_GET ,
145+ // @ts -expect-error FIXME: should work
146+ set : param . set || PATH_PARAM_DEFAULT_SET ,
147+ }
148+ }
149+ }
150+
151+ /**
152+ * Match path against the pattern and return
153+ *
154+ * @param path - path to match
155+ * @throws if the patch doesn't match
156+ * @returns matched decoded params
157+ */
158+ match ( path : string ) : TParams {
159+ const match = path . match ( this . re )
160+ if ( ! match ) {
161+ throw miss ( )
162+ }
163+ let i = 1 // index in match array
164+ const params = { } as TParams
165+ for ( const paramName in this . params ) {
166+ const currentParam = this . params [ paramName ]
167+ const currentMatch = match [ i ++ ]
168+ let value : string | null | string [ ] =
169+ this . opts . optional && currentMatch == null ? null : currentMatch
170+ value = this . opts . repeat && value ? value . split ( '/' ) : value
171+
172+ params [ paramName as keyof typeof params ] = currentParam . get (
173+ // @ts -expect-error: FIXME: the type of currentParam['get'] is wrong
174+ value && ( Array . isArray ( value ) ? value . map ( decode ) : decode ( value ) )
175+ ) as ( typeof params ) [ keyof typeof params ]
176+ }
177+
178+ if ( __DEV__ && i !== match . length ) {
179+ console . warn (
180+ `Regexp matched ${ match . length } params, but ${ i } params are defined`
181+ )
182+ }
183+ return params
184+ }
185+
186+ // build(params: TParams): string {
187+ // let path = this.re.source
188+ // for (const param of this.params) {
189+ // const value = params[param.name as keyof TParams]
190+ // if (value == null) {
191+ // throw new Error(`Matcher build: missing param ${param.name}`)
192+ // }
193+ // path = path.replace(
194+ // /([^\\]|^)\([^?]*\)/,
195+ // `$1${encodeParam(param.set(value))}`
196+ // )
197+ // }
198+ // return path
199+ // }
200+ }
201+
51202export interface MatcherPatternQuery <
52203 TParams extends MatcherParamsFormatted = MatcherParamsFormatted
53204> extends MatcherPatternParams_Base < MatcherQueryParams , TParams > { }
0 commit comments