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
111 changes: 110 additions & 1 deletion base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ export type ParseOptions = {
//=> {foo: ['1', '2', '3']}
```
*/
readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'bracket-separator' | 'colon-list-separator' | 'none';
readonly arrayFormat?:
| 'bracket'
| 'index'
| 'comma'
| 'separator'
| 'bracket-separator'
| 'colon-list-separator'
| 'none';

/**
The character used to separate array elements when using `{arrayFormat: 'separator'}`.
Expand Down Expand Up @@ -169,6 +176,108 @@ export type ParseOptions = {
```
*/
readonly parseFragmentIdentifier?: boolean;

/**
Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over options such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.

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

It is possible to provide a custom function as the parameter type. The parameter's value will equal the function's return value (see example 4).

scottenock marked this conversation as resolved.
Show resolved Hide resolved
NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none` (see example 5).

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
Array types will have no effect when `arrayFormat` is set to `none`
```
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
arrayFormat: 'none',
types: {
ids: 'number[]',
foods: 'string[]',
},
}
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
```

@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&numbers=1%2C2%2C3&double=5&number=20', {
arrayFormat: 'comma',
types: {
ids: 'string',
items: 'string[]',
price: 'string',
numbers: 'number[]',
double: (value) => value * 2,
number: 'number',
},
});
//=> {ids: '001,002,003', items: ['1', '2', '3'], price: '22.00', numbers: [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
34 changes: 26 additions & 8 deletions base.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import decodeComponent from 'decode-uri-component';
import splitOnFirst from 'split-on-first';
import {includeKeys} from 'filter-obj';
import splitOnFirst from 'split-on-first';

const isNullOrUndefined = value => value === null || value === undefined;

Expand Down Expand Up @@ -300,11 +300,25 @@ function getHash(url) {
return hash;
}

function parseValue(value, options) {
function parseValue(value, options, type) {
if (type === 'string' && typeof value === 'string') {
return value;
}

if (typeof type === 'function' && typeof value === 'string') {
return type(value);
}

if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
return value.toLowerCase() === 'true';
}

if (type === 'number' && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
return Number(value);
}

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';
return Number(value);
}

return value;
Expand All @@ -328,6 +342,7 @@ export function parse(query, options) {
arrayFormatSeparator: ',',
parseNumbers: false,
parseBooleans: false,
types: Object.create(null),
...options,
};

Expand Down Expand Up @@ -368,12 +383,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
108 changes: 108 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,114 @@ queryString.parse('foo=true', {parseBooleans: true});

Parse the value as a boolean type instead of string type if it's a boolean.

##### types

Type: `object`\
Default: `{}`

Specify a pre-defined schema to be used when parsing values. The types specified will take precedence over options such as: `parseNumber`, `parseBooleans`, and `arrayFormat`.

Use this feature to override the type of a value. This can be useful when the type is ambiguous such as a phone number.

It is possible to provide a custom function as the parameter type. The parameter's value will equal the function's return value.

Supported Types:

- `'string'`: Parse `phoneNumber` as a string (overriding the `parseNumber` option):

```js
import queryString from 'query-string';

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

- `'number'`: Parse `age` as a number (even when `parseNumber` is false):

```js
import queryString from 'query-string';

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

- `'string[]'`: Parse `items` as an array of strings (overriding the `parseNumber` option):

```js
import queryString from 'query-string';

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

- `'number[]'`: Parse `items` as an array of numbers (even when `parseNumber` is false):

```js
import queryString from 'query-string';

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

- `'Function'`: Provide a custom function as the parameter type. The parameter's value will equal the function's return value.

```js
import queryString from 'query-string';

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

NOTE: Array types (`string[]` and `number[]`) will have no effect if `arrayFormat` is set to `none`.

```js
queryString.parse('ids=001%2C002%2C003&foods=apple%2Corange%2Cmango', {
arrayFormat: 'none',
types: {
ids: 'number[]',
foods: 'string[]',
},
}
//=> {ids:'001,002,003', foods:'apple,orange,mango'}
```

###### Function

```js
import queryString from 'query-string';

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

Parse the value as a boolean type instead of string type if it's a boolean.

### .stringify(object, options?)

Stringify an object into a query string and sorting the keys.
Expand Down
Loading