* @param text - string to encode
* @returns encoded string
*/
-export function commonEncode(text: string | number): string {
- return encodeURI('' + text)
- .replace(ENC_PIPE_RE, '|')
- .replace(ENC_BRACKET_OPEN_RE, '[')
- .replace(ENC_BRACKET_CLOSE_RE, ']')
+export function commonEncode(text: string | number | null | undefined): string {
+ return text == null
+ ? ''
+ : encodeURI('' + text)
+ .replace(ENC_PIPE_RE, '|')
+ .replace(ENC_BRACKET_OPEN_RE, '[')
+ .replace(ENC_BRACKET_CLOSE_RE, ']')
}
/**
* @param text - string to encode
* @returns encoded string
*/
-export function encodePath(text: string | number): string {
+export function encodePath(text: string | number | null | undefined): string {
return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F')
}
* @returns encoded string
*/
export function encodeParam(text: string | number | null | undefined): string {
- return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F')
+ return encodePath(text).replace(SLASH_RE, '%2F')
}
/**
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.match('/teams//b')).toThrow()
expect(pattern.build({ teamId: '123' })).toBe('/teams/123/b')
expect(pattern.build({ teamId: null })).toBe('/teams/b')
+ expect(pattern.build({ teamId: '' })).toBe('/teams/b')
})
it('repeatable param', () => {
expect(pattern.build({ teamId: ['123', '456'] })).toBe('/teams/123/456/b')
})
+ it('catch all route', () => {
+ // const pattern = new MatcherPatternPathDynamic(
+ })
+
+ it('splat params with prefix', () => {
+ const pattern = new MatcherPatternPathDynamic(
+ /^\/teams\/(.*)$/i,
+ {
+ pathMatch: {},
+ },
+ ['teams', 1]
+ )
+ expect(pattern.match('/teams/')).toEqual({ pathMatch: '' })
+ expect(pattern.match('/teams/123/b')).toEqual({ pathMatch: '123/b' })
+ expect(() => pattern.match('/teams')).toThrow()
+ expect(() => pattern.match('/teamso/123/c')).toThrow()
+
+ expect(pattern.build({ pathMatch: null })).toBe('/teams/')
+ expect(pattern.build({ pathMatch: '' })).toBe('/teams/')
+ expect(pattern.build({ pathMatch: '124' })).toBe('/teams/124')
+ expect(pattern.build({ pathMatch: '124/b' })).toBe('/teams/124/b')
+ })
+
it('repeatable optional param', () => {
const pattern = new MatcherPatternPathDynamic(
/^\/teams(?:\/(.+?))?\/b$/i,
import { identityFn } from '../../../utils'
-import { decode, encodeParam } from '../../../encoding'
+import { decode, encodeParam, encodePath } from '../../../encoding'
import { warn } from '../../../warning'
import { miss } from './errors'
import { ParamParser } from './param-parsers/types'
TIn extends string | string[] | null = string | string[] | null,
TOut = string | string[] | null,
> extends ParamParser<TOut, TIn> {
+ /**
+ * Is tha param a repeatable param and should be converted to an array
+ */
repeat?: boolean
+
// NOTE: not needed because in the regexp, the value is undefined if
// the group is optional and not given
// optional?: boolean
// otherwise, we need to use a factory function: https://github.com/microsoft/TypeScript/issues/40451
readonly params: TParamsOptions &
Record<string, MatcherPatternPathDynamic_ParamOptions<any, any>>,
- // 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
+ // 0 means a regular param, 1 means a splat, the order comes from the keys in params
readonly pathParts: Array<string | number | Array<string | number>>
) {
this.paramsKeys = Object.keys(this.params) as Array<keyof TParamsOptions>
build(params: ExtractParamTypeFromOptions<TParamsOptions>): string {
let paramIndex = 0
- return (
+ let paramName: keyof TParamsOptions
+ let paramOptions: (TParamsOptions &
+ Record<
+ string,
+ MatcherPatternPathDynamic_ParamOptions<any, any>
+ >)[keyof TParamsOptions]
+ let lastParamPart: number | undefined
+ let value: ReturnType<NonNullable<ParamParser['set']>> | undefined
+ const path =
'/' +
this.pathParts
.map(part => {
if (typeof part === 'string') {
return part
} else if (typeof part === 'number') {
- const paramName = this.paramsKeys[paramIndex++]
- const paramOptions = this.params[paramName]
- const value: ReturnType<NonNullable<ParamParser['set']>> = (
- paramOptions.set || identityFn
- )(params[paramName])
+ paramName = this.paramsKeys[paramIndex++]
+ paramOptions = this.params[paramName]
+ lastParamPart = part
+ value = (paramOptions.set || identityFn)(params[paramName])
return Array.isArray(value)
? value.map(encodeParam).join('/')
- : encodeParam(value)
+ : // part == 0 means a regular param, 1 means a splat
+ (part /* part !== 0 */ ? encodePath : encodeParam)(value)
} else {
return part
.map(subPart => {
return subPart
}
- const paramName = this.paramsKeys[paramIndex++]
- const paramOptions = this.params[paramName]
- const value: ReturnType<NonNullable<ParamParser['set']>> = (
- paramOptions.set || identityFn
- )(params[paramName])
+ paramName = this.paramsKeys[paramIndex++]
+ paramOptions = this.params[paramName]
+ value = (paramOptions.set || identityFn)(params[paramName])
return Array.isArray(value)
? value.map(encodeParam).join('/')
})
.filter(identityFn) // filter out empty values
.join('/')
- )
+
+ /**
+ * If the last part of the path is a splat param and its value is empty, it gets
+ * filteretd out, resulting in a path that doesn't end with a `/` and doesn't even match
+ * with the original splat path: e.g. /teams/[...pathMatch] does not match /teams, so it makes
+ * no sense to build a path it cannot match.
+ */
+ return lastParamPart && !value ? path + '/' : path
}
}