Skip to content

Commit

Permalink
Merge pull request #14511 from storybookjs/laxer-url-params
Browse files Browse the repository at this point in the history
Core: Allow string in object arg and support fractional numbers in URL args
  • Loading branch information
shilman authored Apr 8, 2021
2 parents 8a40dbd + 67a1dd0 commit 36b34d8
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 29 deletions.
9 changes: 9 additions & 0 deletions lib/client-api/src/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ describe('mapArgsToTypes', () => {

it('maps numbers', () => {
expect(mapArgsToTypes({ a: '42' }, { a: { type: numberType } })).toStrictEqual({ a: 42 });
expect(mapArgsToTypes({ a: '4.2' }, { a: { type: numberType } })).toStrictEqual({ a: 4.2 });
expect(mapArgsToTypes({ a: 'a' }, { a: { type: numberType } })).toStrictEqual({ a: NaN });
});

Expand Down Expand Up @@ -86,6 +87,14 @@ describe('mapArgsToTypes', () => {
});
});

it('passes string for object type', () => {
expect(mapArgsToTypes({ a: 'A' }, { a: { type: boolObjectType } })).toStrictEqual({ a: 'A' });
});

it('passes number for object type', () => {
expect(mapArgsToTypes({ a: 1.2 }, { a: { type: boolObjectType } })).toStrictEqual({ a: 1.2 });
});

it('deeply maps objects', () => {
expect(
mapArgsToTypes(
Expand Down
1 change: 1 addition & 0 deletions lib/client-api/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const map = (arg: unknown, type: ValueType): any => {
return acc;
}, new Array(arg.length));
case 'object':
if (typeof arg === 'string' || typeof arg === 'number') return arg;
if (!type.value || typeof arg !== 'object') return INCOMPATIBLE;
return Object.entries(arg).reduce((acc, [key, val]) => {
const mapped = map(val, (type.value as ObjectValueType)[key]);
Expand Down
58 changes: 33 additions & 25 deletions lib/core-client/src/preview/parseArgsParam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,60 +71,60 @@ describe('parseArgsParam', () => {
});

it('parses multiple values', () => {
const args = parseArgsParam('one:1;two:2;three:3');
expect(args).toStrictEqual({ one: '1', two: '2', three: '3' });
const args = parseArgsParam('one:A;two:B;three:C');
expect(args).toStrictEqual({ one: 'A', two: 'B', three: 'C' });
});

it('parses arrays', () => {
const args = parseArgsParam('arr[]:1;arr[]:2;arr[]:3');
expect(args).toStrictEqual({ arr: ['1', '2', '3'] });
const args = parseArgsParam('arr[]:A;arr[]:B;arr[]:C');
expect(args).toStrictEqual({ arr: ['A', 'B', 'C'] });
});

it('parses arrays with indices', () => {
const args = parseArgsParam('arr[0]:1;arr[1]:2;arr[2]:3');
expect(args).toStrictEqual({ arr: ['1', '2', '3'] });
const args = parseArgsParam('arr[0]:A;arr[1]:B;arr[2]:C');
expect(args).toStrictEqual({ arr: ['A', 'B', 'C'] });
});

it('parses sparse arrays', () => {
const args = parseArgsParam('arr[0]:1;arr[2]:3');
const args = parseArgsParam('arr[0]:A;arr[2]:C');
// eslint-disable-next-line no-sparse-arrays
expect(args).toStrictEqual({ arr: ['1', , '3'] });
expect(args).toStrictEqual({ arr: ['A', , 'C'] });
});

it('parses repeated values as arrays', () => {
const args = parseArgsParam('arr:1;arr:2;arr:3');
expect(args).toStrictEqual({ arr: ['1', '2', '3'] });
const args = parseArgsParam('arr:A;arr:B;arr:C');
expect(args).toStrictEqual({ arr: ['A', 'B', 'C'] });
});

it('parses simple objects', () => {
const args = parseArgsParam('obj.one:1;obj.two:2');
expect(args).toStrictEqual({ obj: { one: '1', two: '2' } });
const args = parseArgsParam('obj.one:A;obj.two:B');
expect(args).toStrictEqual({ obj: { one: 'A', two: 'B' } });
});

it('parses nested objects', () => {
const args = parseArgsParam('obj.foo.one:1;obj.foo.two:2;obj.bar.one:1');
expect(args).toStrictEqual({ obj: { foo: { one: '1', two: '2' }, bar: { one: '1' } } });
const args = parseArgsParam('obj.foo.one:A;obj.foo.two:B;obj.bar.one:A');
expect(args).toStrictEqual({ obj: { foo: { one: 'A', two: 'B' }, bar: { one: 'A' } } });
});

it('parses arrays in objects', () => {
expect(parseArgsParam('obj.foo[]:1;obj.foo[]:2')).toStrictEqual({ obj: { foo: ['1', '2'] } });
expect(parseArgsParam('obj.foo[0]:1;obj.foo[1]:2')).toStrictEqual({ obj: { foo: ['1', '2'] } });
expect(parseArgsParam('obj.foo[]:A;obj.foo[]:B')).toStrictEqual({ obj: { foo: ['A', 'B'] } });
expect(parseArgsParam('obj.foo[0]:A;obj.foo[1]:B')).toStrictEqual({ obj: { foo: ['A', 'B'] } });
// eslint-disable-next-line no-sparse-arrays
expect(parseArgsParam('obj.foo[1]:2')).toStrictEqual({ obj: { foo: [, '2'] } });
expect(parseArgsParam('obj.foo:1;obj.foo:2')).toStrictEqual({ obj: { foo: ['1', '2'] } });
expect(parseArgsParam('obj.foo[1]:B')).toStrictEqual({ obj: { foo: [, 'B'] } });
expect(parseArgsParam('obj.foo:A;obj.foo:B')).toStrictEqual({ obj: { foo: ['A', 'B'] } });
});

it('parses single object in array', () => {
const args = parseArgsParam('arr[].one:1;arr[].two:2');
expect(args).toStrictEqual({ arr: [{ one: '1', two: '2' }] });
const args = parseArgsParam('arr[].one:A;arr[].two:B');
expect(args).toStrictEqual({ arr: [{ one: 'A', two: 'B' }] });
});

it('parses multiple objects in array', () => {
expect(parseArgsParam('arr[0].key:1;arr[1].key:2')).toStrictEqual({
arr: [{ key: '1' }, { key: '2' }],
expect(parseArgsParam('arr[0].key:A;arr[1].key:B')).toStrictEqual({
arr: [{ key: 'A' }, { key: 'B' }],
});
expect(parseArgsParam('arr[0][key]:1;arr[1][key]:2')).toStrictEqual({
arr: [{ key: '1' }, { key: '2' }],
expect(parseArgsParam('arr[0][key]:A;arr[1][key]:B')).toStrictEqual({
arr: [{ key: 'A' }, { key: 'B' }],
});
});

Expand Down Expand Up @@ -222,7 +222,15 @@ describe('parseArgsParam', () => {
expect(parseArgsParam('key:_val_')).toStrictEqual({ key: '_val_' });
expect(parseArgsParam('key:-val-')).toStrictEqual({ key: '-val-' });
expect(parseArgsParam('key:VAL123')).toStrictEqual({ key: 'VAL123' });
expect(parseArgsParam('key:1')).toStrictEqual({ key: '1' });
});

it('allows and parses valid (fractional) numbers', () => {
expect(parseArgsParam('key:1')).toStrictEqual({ key: 1 });
expect(parseArgsParam('key:1.2')).toStrictEqual({ key: 1.2 });
expect(parseArgsParam('key:-1.2')).toStrictEqual({ key: -1.2 });
expect(parseArgsParam('key:1.')).toStrictEqual({});
expect(parseArgsParam('key:.2')).toStrictEqual({});
expect(parseArgsParam('key:1.2.3')).toStrictEqual({});
});

it('also applies to nested object and array values', () => {
Expand Down
12 changes: 10 additions & 2 deletions lib/core-client/src/preview/parseArgsParam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import isPlainObject from 'lodash/isPlainObject';

// Keep this in sync with validateArgs in router/src/utils.ts
const VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/;
const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/;
const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i;
const COLOR_REGEXP = /^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i;
const validateArgs = (key = '', value: unknown): boolean => {
Expand All @@ -14,8 +15,14 @@ const validateArgs = (key = '', value: unknown): boolean => {
if (value === null || value === undefined) return true; // encoded as `!null` or `!undefined`
if (value instanceof Date) return true; // encoded as modified ISO string
if (typeof value === 'number' || typeof value === 'boolean') return true;
if (typeof value === 'string')
return VALIDATION_REGEXP.test(value) || HEX_REGEXP.test(value) || COLOR_REGEXP.test(value);
if (typeof value === 'string') {
return (
VALIDATION_REGEXP.test(value) ||
NUMBER_REGEXP.test(value) ||
HEX_REGEXP.test(value) ||
COLOR_REGEXP.test(value)
);
}
if (Array.isArray(value)) return value.every((v) => validateArgs(key, v));
if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v));
return false;
Expand Down Expand Up @@ -48,6 +55,7 @@ const QS_OPTIONS = {
: `${color[1]}(${color[2]}, ${color[3]}%, ${color[4]}%)`;
}
}
if (type === 'value' && NUMBER_REGEXP.test(str)) return Number(str);
return defaultDecoder(str, defaultDecoder, charset);
},
};
Expand Down
11 changes: 9 additions & 2 deletions lib/router/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const deepDiff = (value: any, update: any): any => {

// Keep this in sync with validateArgs in core-client/src/preview/parseArgsParam.ts
const VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/;
const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/;
const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i;
const COLOR_REGEXP = /^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i;
const validateArgs = (key = '', value: unknown): boolean => {
Expand All @@ -71,8 +72,14 @@ const validateArgs = (key = '', value: unknown): boolean => {
if (value === null || value === undefined) return true; // encoded as `!null` or `!undefined`
if (value instanceof Date) return true; // encoded as modified ISO string
if (typeof value === 'number' || typeof value === 'boolean') return true;
if (typeof value === 'string')
return VALIDATION_REGEXP.test(value) || HEX_REGEXP.test(value) || COLOR_REGEXP.test(value);
if (typeof value === 'string') {
return (
VALIDATION_REGEXP.test(value) ||
NUMBER_REGEXP.test(value) ||
HEX_REGEXP.test(value) ||
COLOR_REGEXP.test(value)
);
}
if (Array.isArray(value)) return value.every((v) => validateArgs(key, v));
if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v));
return false;
Expand Down

0 comments on commit 36b34d8

Please sign in to comment.