Skip to content

Commit

Permalink
feat: Enhance URL query parsing functions (#80)
Browse files Browse the repository at this point in the history
* feat: enhance query string parsing with new functions

- Updated the parseQueryString function to return undefined instead of null for null inputs, improving consistency in return values.
- Introduced parseQueryStringArray function to convert URL query parameter values into an array of strings, with error handling for invalid inputs.
- Added parseQueryPositiveInts function to convert query parameter values into an array of positive integers, including robust error handling and type safety.
- Enhanced documentation with detailed JSDoc comments for new functions, clarifying parameters, return values, and error handling scenarios.

* feat: enhance URL query parameter parsing with detailed documentation

- Updated parseQueryString, parseQueryStringArray, parseQueryNumber, parseQueryInt, and parseQueryPositiveInt functions to improve handling of null, undefined, and invalid inputs.
- Added comprehensive JSDoc comments in both English and Chinese for all functions, clarifying parameters, return values, and error handling scenarios.
- Introduced examples for each function to demonstrate usage and expected outcomes, enhancing overall documentation quality.

* feat: add url-parse module for enhanced query parameter handling

- Introduced a new module `url-parse.ts` with functions for parsing URL query parameters, including `parseQueryString`, `parseQueryStringArray`, `parseQueryNumber`, `parseQueryInt`, and `parseQueryPositiveInt`.
- Added comprehensive JSDoc documentation in both English and Chinese, detailing function parameters, return values, and error handling.
- Created unit tests in `url-parse.test.ts` to validate the functionality of the new parsing methods, ensuring robust handling of various input scenarios.
- Updated `deno.json` to include the new `url-parse` module, enhancing the project's overall functionality.

* feat: expand URL query parameter parsing with new functions and tests

- Added new parsing functions: `parseQueryInt`, `parseQueryNumber`, `parseQueryPositiveInts`, and `parseQueryStringArray` to enhance query parameter handling.
- Updated existing tests for `parseQueryPositiveInt` to improve error handling and consistency in return values.
- Introduced comprehensive unit tests for the new functions, ensuring robust validation of various input scenarios.
- Enhanced overall test coverage for URL query parameter parsing functionality.
  • Loading branch information
iugo authored Dec 18, 2024
1 parent 3e0732f commit 92281ed
Show file tree
Hide file tree
Showing 5 changed files with 376 additions and 161 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"./js/lz-string": "./js/lz-string.ts",
"./js/parse-env": "./js/parse-env.ts",
"./js/ua": "./js/ua.ts",
"./js/url-parse": "./js/url-parse.ts",
"./js/urltest": "./js/urltest.ts",
"./ts/binary": "./ts/binary.ts",
"./ts/error": "./ts/error.ts",
Expand All @@ -26,7 +27,6 @@
"./ts/object": "./ts/object.ts",
"./ts/string": "./ts/string.ts",
"./ts/time": "./ts/time.ts",
"./ts/url": "./ts/url.ts",
"./deno/copy": "./deno/copy.ts",
"./deno/git": "./deno/git.ts",
"./deno/run": "./deno/run.ts",
Expand Down
146 changes: 146 additions & 0 deletions js/url-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { assertEquals, assertThrows } from '@std/assert';
import {
parseQueryInt,
parseQueryNumber,
parseQueryPositiveInt,
parseQueryPositiveInts,
parseQueryString,
parseQueryStringArray,
} from './url-parse.ts';

Deno.test('parseQueryString', () => {
const url = new URL(
'https://example.com/path?a=1&b=2&c=&d=d&e=null&f=undefined',
);
url.searchParams.set('b', '中');
url.searchParams.set('g', ' ');
url.searchParams.set('h', ';abc');
const a = url.searchParams.get('a');
const b = url.searchParams.get('b');
const c = url.searchParams.get('c');
const d = url.searchParams.get('d');
const e = url.searchParams.get('e');
const f = url.searchParams.get('f');
const g = url.searchParams.get('g');
console.log({ a, b, c, d, e, f, g });
assertEquals(parseQueryString(a), '1');
assertEquals(parseQueryString(b), '中');
assertEquals(parseQueryString(c), undefined);
assertEquals(parseQueryString(d), 'd');
assertEquals(parseQueryString(e), 'null');
assertEquals(parseQueryString(f), undefined);
assertEquals(parseQueryString(g), undefined);
assertThrows(() => parseQueryString(';abc'), TypeError);
});

Deno.test('parseQueryPositiveInt', () => {
const url = new URL('https://example.com/path');
url.searchParams.set('a', '1');
url.searchParams.set('b', '2');
url.searchParams.set('c', '');
url.searchParams.set('d', '0');
url.searchParams.set('e', 'null');
url.searchParams.set('g', '-1');
url.searchParams.set('h', '1.5');
url.searchParams.set('i', 'abc');
const a = url.searchParams.get('a');
const b = url.searchParams.get('b');
const c = url.searchParams.get('c');
const d = url.searchParams.get('d');
const e = url.searchParams.get('e');
const g = url.searchParams.get('g');
const h = url.searchParams.get('h');
const i = url.searchParams.get('i');

assertEquals(parseQueryPositiveInt(a), 1);
assertEquals(parseQueryPositiveInt(b), 2);
assertEquals(parseQueryPositiveInt(c), undefined);
assertThrows(() => parseQueryPositiveInt(d), TypeError); // 0 不是正整数
assertThrows(() => parseQueryPositiveInt(e), TypeError);
assertThrows(() => parseQueryPositiveInt(g), TypeError); // 负数不是正整数
assertThrows(() => parseQueryPositiveInt(h), TypeError); // 小数不是整数
assertThrows(() => parseQueryPositiveInt(i), TypeError); // 非数字字符串
});

Deno.test('parseQueryStringArray', () => {
const url = new URL('https://example.com/path');
url.searchParams.set('a', 'a,b,c');
url.searchParams.set('b', 'x|y|z');
url.searchParams.set('c', '');
url.searchParams.set('d', 'a,,c');
url.searchParams.set('e', '<script>,b,c');

assertEquals(parseQueryStringArray(url.searchParams.get('a')), [
'a',
'b',
'c',
]);
assertEquals(
parseQueryStringArray(url.searchParams.get('b'), { separator: '|' }),
['x', 'y', 'z'],
);
assertEquals(parseQueryStringArray(url.searchParams.get('c')), undefined);
assertEquals(parseQueryStringArray(url.searchParams.get('d')), ['a', 'c']);
assertThrows(
() => parseQueryStringArray(url.searchParams.get('e')),
TypeError,
);
});

Deno.test('parseQueryNumber', () => {
const url = new URL('https://example.com/path');
url.searchParams.set('a', '123');
url.searchParams.set('b', '12.34');
url.searchParams.set('c', '');
url.searchParams.set('d', 'abc');
url.searchParams.set('e', 'Infinity');
url.searchParams.set('f', 'NaN');

assertEquals(parseQueryNumber(url.searchParams.get('a')), 123);
assertEquals(parseQueryNumber(url.searchParams.get('b')), 12.34);
assertEquals(parseQueryNumber(url.searchParams.get('c')), undefined);
assertThrows(() => parseQueryNumber(url.searchParams.get('d')), TypeError);
assertThrows(() => parseQueryNumber(url.searchParams.get('e')), TypeError);
assertThrows(() => parseQueryNumber(url.searchParams.get('f')), TypeError);
});

Deno.test('parseQueryInt', () => {
const url = new URL('https://example.com/path');
url.searchParams.set('a', '123');
url.searchParams.set('b', '12.34');
url.searchParams.set('c', '');
url.searchParams.set('d', 'abc');
url.searchParams.set('e', '-123');
url.searchParams.set('f', '0');

assertEquals(parseQueryInt(url.searchParams.get('a')), 123);
assertThrows(() => parseQueryInt(url.searchParams.get('b')), TypeError);
assertEquals(parseQueryInt(url.searchParams.get('c')), undefined);
assertThrows(() => parseQueryInt(url.searchParams.get('d')), TypeError);
assertEquals(parseQueryInt(url.searchParams.get('e')), -123);
assertEquals(parseQueryInt(url.searchParams.get('f')), 0);
});

Deno.test('parseQueryPositiveInts', () => {
const url = new URL('https://example.com/path');
url.searchParams.set('a', '1,2,3');
url.searchParams.set('b', '');
url.searchParams.set('c', '1,0,3');
url.searchParams.set('d', '1,-2,3');
url.searchParams.set('e', '1,abc,3');

assertEquals(parseQueryPositiveInts(url.searchParams.get('a')), [1, 2, 3]);
assertEquals(parseQueryPositiveInts(url.searchParams.get('b')), undefined);
assertThrows(
() => parseQueryPositiveInts(url.searchParams.get('c')),
TypeError,
);
assertThrows(
() => parseQueryPositiveInts(url.searchParams.get('d')),
TypeError,
);
assertThrows(
() => parseQueryPositiveInts(url.searchParams.get('e')),
TypeError,
);
});
229 changes: 229 additions & 0 deletions js/url-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import { toInt, toPositiveInt } from '../ts/number-type-convert.ts';
import { isSafeString, SafeString } from '../ts/string.ts';

/**
* Convert URL query parameter value to string
* 将 URL 查询参数值转换为字符串
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* 可以为 null 或字符串类型
* @returns A SafeString or undefined
* 返回安全字符串或 undefined
* - Returns undefined if input is null, "undefined" or empty string
* 当输入为 null、"undefined" 或空字符串时返回 undefined
* - Returns trimmed SafeString for valid input
* 对于有效输入返回经过去空格处理的安全字符串
* - Throws TypeError for invalid input containing unsafe characters
* 当输入包含不安全字符时抛出 TypeError
*
* @example
* parseQueryString('hello') // returns 'hello'
* parseQueryString(null) // returns undefined
* parseQueryString('') // returns undefined
* parseQueryString('<script>') // throws TypeError
*
* @author iugo <[email protected]>
*/
export function parseQueryString(
query: string | null,
): SafeString | undefined {
if (query === null) {
return undefined;
}
const trimmedQuery = query.trim();
if (trimmedQuery === 'undefined' || trimmedQuery === '') {
return undefined;
}
if (!isSafeString(trimmedQuery)) {
throw new TypeError(`invalid query string: ${query}`);
}
return trimmedQuery;
}

/**
* Convert URL query parameter value to array of strings
* 将 URL 查询参数值转换为字符串数组
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* 可以为 null 或字符串类型
* @param options - Configuration options
* 配置选项
* @param options.separator - Delimiter used to split the string (默认为逗号 ',')
* 用于分割字符串的分隔符
* @returns An array of strings or undefined
* 返回字符串数组或 undefined
* - Returns undefined if input is null, "undefined", empty string or results in empty array
* 当输入为 null、"undefined"、空字符串或结果为空数组时返回 undefined
* - Returns array of non-empty strings for valid input
* 对于有效输入返回非空字符串数组
* - Throws TypeError if any element contains unsafe characters
* 当任何元素包含不安全字符时抛出 TypeError
*
* @example
* parseQueryStringArray('a,b,c') // returns ['a', 'b', 'c']
* parseQueryStringArray('a|b|c', { separator: '|' }) // returns ['a', 'b', 'c']
* parseQueryStringArray('') // returns undefined
*
* @author iugo <[email protected]>
*/
export function parseQueryStringArray(
query: string | null,
{ separator = ',' }: { separator?: string } = {},
): string[] | undefined {
if (query === null || query === 'undefined' || query === '') {
return undefined;
}

try {
const arr = query
.split(separator)
.map((v) => parseQueryString(v) ?? '')
.filter(Boolean);

return arr.length === 0 ? undefined : arr;
} catch (_err) {
throw new TypeError(`invalid query string array: ${query}`);
}
}

/**
* Convert URL query parameter value to number
* 将 URL 查询参数值转换为数字
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* 可以为 null 或字符串类型
* @returns A number or undefined
* 返回数字或 undefined
* - Returns undefined if input is null, "undefined" or empty string
* 当输入为 null、"undefined" 或空字符串时返回 undefined
* - Returns parsed number for valid numeric input (包括整数和浮点数)
* 对于有效的数字输入返回解析后的数字(包括整数和浮点数)
* - Throws TypeError for non-numeric input or NaN/Infinity values
* 当输入非数字或为 NaN/Infinity 时抛出 TypeError
*
* @example
* parseQueryNumber('123') // returns 123
* parseQueryNumber('12.34') // returns 12.34
* parseQueryNumber('abc') // throws TypeError
*
* @author iugo <[email protected]>
*/
export function parseQueryNumber(
query: string | null,
): number | undefined {
if (query === null || query === 'undefined' || query === '') {
return undefined;
}
const trimmedQuery = query.trim();
const num = Number(trimmedQuery);
if (!Number.isFinite(num)) {
throw new TypeError(`invalid query number: ${query}`);
}
return num;
}

/**
* Convert URL query parameter value to integer
* 将 URL 查询参数值转换为整数
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* @returns A number or undefined
* 返回整数或 undefined
* - Returns undefined if input is null, "undefined" or empty string
* 当输入为 null、"undefined" 或空字符串时返回 undefined
* - Returns integer for valid numeric input
* 对于有效的数字输入返回整数
* - Throws TypeError for invalid input
* 当输入无效时抛出 TypeError
*
* @example
* parseQueryInt('123') // returns 123
* parseQueryInt('12.34') // returns 12
* parseQueryInt('abc') // throws TypeError
*
* @author iugo <[email protected]>
*/
export function parseQueryInt(query: string | null): number | undefined {
if (query === null || query === 'undefined' || query === '') {
return undefined;
}
const num = parseQueryNumber(query);
if (num === undefined) {
return undefined;
}
return toInt(num);
}

/**
* Convert URL query parameter value to positive integer
* 将 URL 查询参数值转换为正整数
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* @returns A number or undefined
* 返回正整数或 undefined
* - Returns undefined if input is null, "undefined" or empty string
* 当输入为 null、"undefined" 或空字符串时返回 undefined
* - Returns positive integer for valid numeric input
* 对于有效的数字输入返回正整数
* - Throws TypeError for invalid input or non-positive numbers
* 当输入无效或非正数时抛出 TypeError
*
* @example
* parseQueryPositiveInt('123') // returns 123
* parseQueryPositiveInt('-1') // throws TypeError
* parseQueryPositiveInt('0') // throws TypeError
*
* @author iugo <[email protected]>
*/
export function parseQueryPositiveInt(
query: string | null,
): number | undefined {
if (query === null || query === 'undefined' || query === '') {
return undefined;
}
const num = parseQueryNumber(query);
if (num === undefined) {
return undefined;
}
return toPositiveInt(num);
}

/**
* Convert URL query parameter value to array of positive integers
* 将 URL 查询参数值转换为正整数数组
*
* @param query - URL query parameter value (typically from url.searchParams.get())
* URL 查询参数值(通常来自 url.searchParams.get())
* @returns An array of positive integers or undefined
* 返回正整数数组或 undefined
* - Returns undefined if input is null, "undefined" or empty string
* 当输入为 null、"undefined" 或空字符串时返回 undefined
* - Returns array of positive integers for valid input
* 对于有效的数字输入返回正整数数组
* - Throws TypeError for invalid input
* 当输入无效时抛出 TypeError
*
* @example
* parseQueryPositiveInts('1,2,3') // returns [1, 2, 3]
* parseQueryPositiveInts('') // returns undefined
* parseQueryPositiveInts('1|2|3', { separator: '|' }) // returns [1, 2, 3]
*
* @author iugo <[email protected]>
*/
export function parseQueryPositiveInts(
query: string | null,
): number[] | undefined {
if (query === null || query === 'undefined' || query === '') {
return undefined;
}
try {
return query.split(',').map(toPositiveInt);
} catch (_err) {
throw new TypeError(`invalid query positive int array: ${query}`);
}
}
Loading

0 comments on commit 92281ed

Please sign in to comment.