)
expect(matcher.match({ p: '5' })).toEqual({ page: '5' })
})
+
+ it('lets the parser handle null values', () => {
+ expect(
+ new MatcherPatternQueryParam(
+ 'active',
+ 'a',
+ 'value',
+ // transforms null to true
+ PARAM_PARSER_BOOL,
+ false
+ ).match({ a: null })
+ ).toEqual({ active: true })
+
+ expect(
+ new MatcherPatternQueryParam(
+ 'active',
+ 'a',
+ 'value',
+ // this leavs the value as null
+ PARAM_PARSER_DEFAULTS,
+ 'ko'
+ ).match({ a: null })
+ ).toEqual({ active: null })
+ })
})
describe('parser integration', () => {
expect(matcher.match({ item: [] })).toEqual({ items: [] })
})
- it('filters out null values in arrays', () => {
+ it('integer parser filters out null values in arrays', () => {
+ const matcher = new MatcherPatternQueryParam(
+ 'ids',
+ 'id',
+ 'array',
+ PARAM_PARSER_INT
+ )
+ // Integer parser filters out null values from arrays
+ expect(matcher.match({ id: ['1', null, '3'] })).toEqual({
+ ids: [1, 3],
+ })
+ })
+
+ it('integer parser with default also filters null values', () => {
const matcher = new MatcherPatternQueryParam(
'ids',
'id',
PARAM_PARSER_INT,
() => []
)
- expect(matcher.match({ id: ['1', null, '3'] })).toEqual({ ids: [1, 3] })
+ // Integer parser filters null values even with default
+ expect(matcher.match({ id: ['1', null, '3'] })).toEqual({
+ ids: [1, 3],
+ })
+ })
+
+ it('passes null values to boolean parser in arrays', () => {
+ const matcher = new MatcherPatternQueryParam(
+ 'flags',
+ 'flag',
+ 'array',
+ PARAM_PARSER_BOOL
+ )
+ // Now that null filtering is removed, null values get passed to parser
+ expect(matcher.match({ flag: ['true', null, 'false'] })).toEqual({
+ flags: [true, true, false],
+ })
+ })
+
+ it('handles null values with default parser in arrays', () => {
+ const matcher = new MatcherPatternQueryParam(
+ 'values',
+ 'value',
+ 'array',
+ PARAM_PARSER_DEFAULTS
+ )
+ // Now that null filtering is removed, null values get passed to parser
+ expect(matcher.match({ value: ['a', null, 'b'] })).toEqual({
+ values: ['a', null, 'b'],
+ })
})
it('handles undefined query param with default', () => {
MatcherParamsFormatted,
MatcherPattern,
MatcherQueryParams,
+ MatcherQueryParamsValue,
} from './matcher-pattern'
import { ParamParser, PARAM_PARSER_DEFAULTS } from './param-parsers'
import { miss } from './errors'
) {}
match(query: MatcherQueryParams): Record<ParamName, T> {
- const queryValue = query[this.queryKey]
+ const queryValue: MatcherQueryParamsValue | undefined = query[this.queryKey]
+
+ // Check if query param is missing for default value handling
let valueBeforeParse =
this.format === 'value'
? Array.isArray(queryValue)
? queryValue[0]
: queryValue
- : this.format === 'array'
- ? Array.isArray(queryValue)
- ? queryValue
+ : // format === 'array'
+ Array.isArray(queryValue)
+ ? queryValue
+ : queryValue == null
+ ? []
: [queryValue]
- : queryValue
let value: T | undefined
- // if we have an array, we need to try catch each value
+ // if we have an array, pass the whole array to the parser
if (Array.isArray(valueBeforeParse)) {
- // @ts-expect-error: T is not connected to valueBeforeParse
- value = []
- for (const v of valueBeforeParse) {
- if (v != null) {
- try {
- ;(value as unknown[]).push(
- // for ts errors
- (this.parser.get ?? PARAM_PARSER_DEFAULTS.get)(v) as T
- )
- } catch (error) {
- // we skip the invalid value unless there is no defaultValue
- if (this.defaultValue === undefined) {
- throw error
- }
+ // for arrays, if original query param was missing and we have a default, use it
+ if (queryValue === undefined && this.defaultValue !== undefined) {
+ value = toValue(this.defaultValue)
+ } else {
+ try {
+ value = (this.parser.get ?? PARAM_PARSER_DEFAULTS.get)(
+ valueBeforeParse
+ ) as T
+ } catch (error) {
+ if (this.defaultValue === undefined) {
+ throw error
}
+ value = undefined
}
}
-
- // if we have no values, we want to fall back to the default value
- if (
- this.defaultValue !== undefined &&
- (value as unknown[]).length === 0
- ) {
- value = undefined
- }
} else {
try {
value =
set: (v: number | null) => (v == null ? null : String(v / 2)),
})
+ const nullAwareParser = definePathParamParser({
+ get: (v: string | null) => {
+ if (v === null) return 'was-null'
+ if (v === undefined) return 'was-undefined'
+ return `processed-${v}`
+ },
+ set: (v: string | null) =>
+ v === 'was-null' ? null : String(v).replace('processed-', ''),
+ })
+
it('single regular param', () => {
const pattern = new MatcherPatternPathDynamic(
/^\/teams\/([^/]+?)$/i,
expect(pattern.build({ teamId: 0 })).toBe('/teams/0')
expect(pattern.build({ teamId: null })).toBe('/teams')
})
+
+ it('handles null values in optional params with custom parser', () => {
+ const pattern = new MatcherPatternPathDynamic(
+ /^\/teams(?:\/([^/]+?))?$/i,
+ {
+ teamId: [nullAwareParser, false, true],
+ },
+ ['teams', 1]
+ )
+
+ expect(pattern.match('/teams')).toEqual({ teamId: 'was-null' })
+ expect(pattern.match('/teams/hello')).toEqual({
+ teamId: 'processed-hello',
+ })
+ expect(pattern.build({ teamId: 'was-null' })).toBe('/teams')
+ expect(pattern.build({ teamId: 'processed-world' })).toBe('/teams/world')
+ })
})
})
expect(PARAM_PARSER_BOOL.get([null, null])).toEqual([true, true])
})
+ it('handles mixed arrays with null values correctly', () => {
+ expect(PARAM_PARSER_BOOL.get([null, 'true', null])).toEqual([
+ true,
+ true,
+ true,
+ ])
+ expect(PARAM_PARSER_BOOL.get(['false', null, 'TRUE'])).toEqual([
+ false,
+ true,
+ true,
+ ])
+ expect(PARAM_PARSER_BOOL.get([null, 'false', null, 'true'])).toEqual([
+ true,
+ false,
+ true,
+ true,
+ ])
+ })
+
it('throws for arrays with invalid values', () => {
expect(() => PARAM_PARSER_BOOL.get(['true', 'invalid'])).toThrow()
expect(() => PARAM_PARSER_BOOL.get(['invalid'])).toThrow()
expect(() => PARAM_PARSER_INT.get(['', '2'])).toThrow()
})
- it('throws for arrays with null values', () => {
- expect(() => PARAM_PARSER_INT.get(['1', null, '3'])).toThrow()
- expect(() => PARAM_PARSER_INT.get([null])).toThrow()
+ it('filters out null values from arrays', () => {
+ expect(PARAM_PARSER_INT.get(['1', null, '3'])).toEqual([1, 3])
+ expect(PARAM_PARSER_INT.get([null])).toEqual([])
+ expect(PARAM_PARSER_INT.get(['42', null, null, '7'])).toEqual([42, 7])
})
})
} satisfies ParamParser<number, string | null>
const PARAM_INTEGER_REPEATABLE = {
- get: (value: (string | null)[]) => value.map(PARAM_INTEGER_SINGLE.get),
+ get: (value: (string | null)[]) =>
+ value.filter((v): v is string => v != null).map(PARAM_INTEGER_SINGLE.get),
set: (value: number[]) => value.map(PARAM_INTEGER_SINGLE.set),
} satisfies ParamParser<number[], (string | null)[]>