1
- import { MatcherName , MatcherQueryParams } from './matcher'
1
+ import { decode , MatcherName , MatcherQueryParams } from './matcher'
2
2
import { EmptyParams , MatcherParamsFormatted } from './matcher-location'
3
3
import { miss } from './matchers/errors'
4
4
@@ -19,14 +19,28 @@ export interface MatcherPatternParams_Base<
19
19
TIn = string ,
20
20
TOut extends MatcherParamsFormatted = MatcherParamsFormatted
21
21
> {
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
+ */
22
29
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
+ */
23
38
build ( params : TOut ) : TIn
24
39
}
25
40
26
41
export 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
30
44
> extends MatcherPatternParams_Base < string , TParams > { }
31
45
32
46
export class MatcherPatternPathStatic
@@ -48,6 +62,143 @@ export class MatcherPatternPathStatic
48
62
// example of a static matcher built at runtime
49
63
// new MatcherPatternPathStatic('/')
50
64
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
+
51
202
export interface MatcherPatternQuery <
52
203
TParams extends MatcherParamsFormatted = MatcherParamsFormatted
53
204
> extends MatcherPatternParams_Base < MatcherQueryParams , TParams > { }
0 commit comments