Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement types schema option parameter #385

Merged
merged 11 commits into from
Jul 22, 2024
86 changes: 86 additions & 0 deletions base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,92 @@ export type ParseOptions = {
```
*/
readonly parseFragmentIdentifier?: boolean;

/**
Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over global parameters such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.
scottenock marked this conversation as resolved.
Show resolved Hide resolved

Use this feature to override the type for a value. This can be useful when the type is ambiguous such as a phone number (see example 1 and 2).

Types specified here will be used even when global parsing options such as `parseNumber`, and `arrayFormat` are not enabled (see example 3).
scottenock marked this conversation as resolved.
Show resolved Hide resolved

scottenock marked this conversation as resolved.
Show resolved Hide resolved
NOTE: array types (`string[]` and `number[]`) will not work if `arrayFormat` is set to `none`.
scottenock marked this conversation as resolved.
Show resolved Hide resolved

sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
@default {}

@example
Parse `phoneNumber` as a string, overriding the `parseNumber` option:
```
import queryString from 'query-string';

queryString.parse('?phoneNumber=%2B380951234567&id=1', {
parseNumbers: true,
types: {
phoneNumber: 'string',
}
});
//=> {phoneNumber: '+380951234567', id: 1}
```

@example
Parse `items` as an array of strings, overriding the `parseNumber` option:
```
import queryString from 'query-string';

queryString.parse('?age=20&items=1%2C2%2C3', {
parseNumber: true,
types: {
items: 'string[]',
}
});
//=> {age: 20, items: ['1', '2', '3']}
```

@example
Parse `age` as a number, even when `parseNumber` is false:
```
import queryString from 'query-string';

queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: 'number',
}
});
//=> {age: 20, id: '01234', zipcode: '90210 }
```

@example
Parse `age` using a custom value parser:
```
import queryString from 'query-string';

queryString.parse('?age=20&id=01234&zipcode=90210', {
types: {
age: (value) => value * 2,
}
});
//=> {age: 40, id: '01234', zipcode: '90210 }
```

@example
Parse a query utilizing all types:
```
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",
scottenock marked this conversation as resolved.
Show resolved Hide resolved
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[]' | ((value: string) => unknown)>;
};

// 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')) {
scottenock marked this conversation as resolved.
Show resolved Hide resolved
return value;
}

if (typeof type === 'function' && (typeof value === 'string')) {
value = type(value);
scottenock marked this conversation as resolved.
Show resolved Hide resolved
} else if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
scottenock marked this conversation as resolved.
Show resolved Hide resolved
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
114 changes: 114 additions & 0 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,117 @@ 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('types option: can override a parsed number to be a string ', t => {
t.deepEqual(queryString.parse('phoneNumber=%2B380951234567', {
parseNumbers: true,
types: {
phoneNumber: 'string',
},
}), {phoneNumber: '+380951234567'});
});

test('types option: can override a parsed boolean value to be a string', t => {
t.deepEqual(queryString.parse('question=true', {
parseBooleans: true,
types: {
question: 'string',
},
}), {
question: 'true',
});
});

test('types option: 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('types option: 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('types option: 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],
});
});

test('types option: 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('types option: when value is not of specified type, it will safely parse the value as string', t => {
t.deepEqual(queryString.parse('id=example', {
types: {
id: 'number',
},
}), {
id: 'example',
});
});

test('types option: 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('types option: 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[]',
scottenock marked this conversation as resolved.
Show resolved Hide resolved
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,
});
});