Skip to content

Commit

Permalink
feat: implement types schema option parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
scottenock committed May 25, 2024
1 parent 3d8fbf2 commit c477f6d
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 5 deletions.
29 changes: 29 additions & 0 deletions base.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type CustomValueParser = (value: string) => unknown;

export type ParseOptions = {
/**
Decode the keys and values. URI components are decoded with [`decode-uri-component`](https://github.com/SamVerschueren/decode-uri-component).
Expand Down Expand Up @@ -169,6 +171,33 @@ export type ParseOptions = {
```
*/
readonly parseFragmentIdentifier?: boolean;

/**
Specify a pre-defined schema to be used when parsing values.

Check failure on line 176 in base.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Trailing spaces not allowed.

Check failure on line 176 in base.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Trailing spaces not allowed.
Use this feature to override configuration options: parseNumber, parseBooleans, and arrayFormat.
pre-defined types specified here will be used even if parsing options such parseNumber as are not enabled.

Check failure on line 178 in base.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Trailing spaces not allowed.

Check failure on line 178 in base.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Trailing spaces not allowed.
@default {}
@example
```
import queryString from 'query-string';
queryString.parse("ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&nums=1%2C2%2C3&double=5&number=20",
{ arrayFormat: "comma", types: {
ids: "string",
items: "string[]",
price: "string",
nums: "number[]",
double: (value) => value * 2,
number: "number",
},
});
//=> {ids: '001,002,003', items: ['1', '2', '3'], price: '22.00', nums: [1, 2, 3], double: 10, number: 20,}
```
*/
readonly types?: Record<string, 'number' | 'string' | 'string[]' | 'number[]' | CustomValueParser>;
};

// eslint-disable-next-line @typescript-eslint/ban-types
Expand Down
22 changes: 17 additions & 5 deletions base.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,16 @@ function getHash(url) {
return hash;
}

function parseValue(value, options) {
if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
function parseValue(value, options, type) {
if (type === 'string' && (typeof value === 'string')) {
return value;
}

if (typeof type === 'function' && (typeof value === 'string')) {
value = type(value);
} else if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
value = Number(value);
} else if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
value = Number(value);
} else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
value = value.toLowerCase() === 'true';
Expand All @@ -328,6 +336,7 @@ export function parse(query, options) {
arrayFormatSeparator: ',',
parseNumbers: false,
parseBooleans: false,
types: Object.create(null),
...options,
};

Expand Down Expand Up @@ -368,12 +377,15 @@ export function parse(query, options) {
}

for (const [key, value] of Object.entries(returnValue)) {
if (typeof value === 'object' && value !== null) {
if (typeof value === 'object' && value !== null && options.types[key] !== 'string') {
for (const [key2, value2] of Object.entries(value)) {
value[key2] = parseValue(value2, options);
const type = options.types[key] ? options.types[key].replace('[]', '') : undefined;
value[key2] = parseValue(value2, options, type);
}
} else if (typeof value === 'object' && value !== null && options.types[key] === 'string') {
returnValue[key] = Object.values(value).join(options.arrayFormatSeparator);
} else {
returnValue[key] = parseValue(value, options);
returnValue[key] = parseValue(value, options, options.types[key]);
}
}

Expand Down
93 changes: 93 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,96 @@ test('query strings having (:list) colon-list-separator arrays', t => {
test('query strings having (:list) colon-list-separator arrays including null values', t => {
t.deepEqual(queryString.parse('bar:list=one&bar:list=two&foo', {arrayFormat: 'colon-list-separator'}), {bar: ['one', 'two'], foo: null});
});

test('pre-defined types parsing: can override a parsed number to be a string ', t => {
t.deepEqual(queryString.parse('phoneNumber=%2B380951234567', {
parseNumbers: true,
types: {
phoneNumber: 'string',
},
}), {phoneNumber: '+380951234567'});
});

test('pre-defined types parsing: can override parsed numbers arrays to be string[]', t => {
t.deepEqual(queryString.parse('ids=999%2C998%2C997&items=1%2C2%2C3', {
arrayFormat: 'comma', parseNumbers: true, types: {
ids: 'string[]',
},
}), {
ids: ['999', '998', '997'],
items: [1, 2, 3],
});
});

test('pre-defined types parsing: can override string arrays to be number[]', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
arrayFormat: 'comma', types: {
ids: 'number[]',
},
}), {
ids: [1, 2, 3],
items: ['1', '2', '3'],
});
});

test('pre-defined types parsing: can override an array to be string', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3', {
arrayFormat: 'comma', parseNumbers: true, types: {
ids: 'string',
},
}), {
ids: '001,002,003',
items: [1, 2, 3],
});
});

Check failure on line 448 in test/parse.js

View workflow job for this annotation

GitHub Actions / Node.js 20

Trailing spaces not allowed.

Check failure on line 448 in test/parse.js

View workflow job for this annotation

GitHub Actions / Node.js 18

Trailing spaces not allowed.

test('pre-defined types parsing: can override a separator array to be string ', t => {
t.deepEqual(queryString.parse('ids=001|002|003&items=1|2|3', {
arrayFormat: 'separator', arrayFormatSeparator: '|', parseNumbers: true, types: {
ids: 'string',
},
}), {
ids: '001|002|003',
items: [1, 2, 3],
});
});

test('pre-defined types parsing: when value is not of specified type, it will safely parse the value as string', t => {
t.deepEqual(queryString.parse('id=example', {
arrayFormat: 'comma', types: {
id: 'number',
},
}), {
id: 'example',
});
});

test('pre-defined types parsing: will parse the value as number if specified in type but parseNumbers is false', t => {
t.deepEqual(queryString.parse('id=123', {
arrayFormat: 'comma', types: {
id: 'number',
},
}), {
id: 123,
});
});

test('pre-defined types parsing: all supported types work in conjunction with one another', t => {
t.deepEqual(queryString.parse('ids=001%2C002%2C003&items=1%2C2%2C3&price=22%2E00&nums=1%2C2%2C3&double=5&number=20', {
arrayFormat: 'comma', types: {
ids: 'string',
items: 'string[]',
price: 'string',
nums: 'number[]',
double: value => value * 2,
number: 'number',
},
}), {
ids: '001,002,003',
items: ['1', '2', '3'],
price: '22.00',
nums: [1, 2, 3],
double: 10,
number: 20,
});
});

0 comments on commit c477f6d

Please sign in to comment.