/** * @description 判断给定值是否为字符串。 * * @example * ``` * // Expect: true * const example1 = internalIsString("page=1") * * // Expect: false * const example2 = internalIsString({ page: "1" }) * ``` */ const internalIsString = (value: unknown): value is string => { return typeof value === "string" } /** * @description 判断给定值是否为普通对象。 * * @example * ``` * // Expect: true * const example1 = internalIsObject({ page: "1" }) * * // Expect: false * const example2 = internalIsObject(["page"]) * ``` */ const internalIsObject = (value: unknown): value is Record => { return typeof value === "object" && value !== null && Array.isArray(value) === false } /** * @description 将单个 query 拆分为 key 和 value。 * * @example * ``` * // Expect: ["page", "1"] * const example1 = internalSplitQueryEntry("page=1") * * // Expect: ["formula", "a=b"] * const example2 = internalSplitQueryEntry("formula=a=b") * ``` */ const internalSplitQueryEntry = (query: string): [key: string, value: string] => { const internalEqualSignIndex = query.indexOf("=") if (internalEqualSignIndex === -1) { const key = query const value = "" return [key, value] } const key = query.slice(0, internalEqualSignIndex) const value = query.slice(internalEqualSignIndex + 1) return [key, value] } /** * @description 不带 `?` 前缀的 query 字符串。 * * @example * ``` * // Expect: "page=1" * const example1: QueryString = "page=1" * ``` */ export type QueryString = string /** * @description 查询参数对象。 * * @example * ``` * // Expect: { page: "1" } * const example1: QueryObject = { page: "1" } * ``` */ export type QueryObject = Record /** * @description 查询参数支持的输入或输出形态。 * * @example * ``` * // Expect: "page=1" * const example1: QueryUnion = "page=1" * * // Expect: { page: "1" } * const example2: QueryUnion = { page: "1" } * ``` */ export type QueryUnion = QueryString | QueryObject export type SearchStringLoose = string /** * @description 带 `?` 前缀的 search 字符串。 * * @example * ``` * // Expect: "?page=1" * const example1: SearchStringStrict = "?page=1" * ``` */ export type SearchStringStrict = `?${string}` export type SearchUnion = QueryString | QueryObject | SearchStringLoose | SearchStringStrict /** * @description 规范化 search 字符串,确保其带有 `?` 前缀。 * * @example * ``` * // Expect: "?name=cigaret" * const example1 = neatenSearch("?name=cigaret") * * // Expect: "?name=cigaret" * const example2 = neatenSearch("name=cigaret") * ``` */ export const neatenSearch = (target: string): SearchStringStrict => { return target.startsWith("?") ? (target as SearchStringStrict) : `?${target}` } /** * @description 规范化 query 字符串,确保其不带 `?` 前缀。 * * @example * ``` * // Expect: "name=cigaret" * const example1 = neatenQueryString("?name=cigaret") * * // Expect: "name=cigaret" * const example2 = neatenQueryString("name=cigaret") * ``` */ export const neatenQueryString = (target: string): QueryString => { return target.startsWith("?") ? target.slice(1) : target } /** * @description 将 query 字符串解析为查询参数对象。 * * @example * ``` * // Expect: { name: "cigaret" } * const example1 = queryStringToQueryObject("name=cigaret") * * // Expect: { formula: "a=b" } * const example2 = queryStringToQueryObject("formula=a%3Db") * ``` */ export const queryStringToQueryObject = (target: QueryString): QueryObject => { const queryString = neatenQueryString(target) const querys = queryString.split("&") const queryObject: QueryObject = {} querys.forEach((query) => { if (query !== "") { const [rawKey, rawValue] = internalSplitQueryEntry(query) queryObject[decodeURIComponent(rawKey)] = decodeURIComponent(rawValue) } }) return queryObject } /** * @description 将查询参数对象序列化为 query 字符串。 * * @example * ``` * // Expect: "name=cigaret" * const example1 = queryObjectToQueryString({ name: "cigaret" }) * * // Expect: "formula=a%3Db" * const example2 = queryObjectToQueryString({ formula: "a=b" }) * ``` */ export const queryObjectToQueryString = (target: QueryObject): QueryString => { const queryString = Object.entries(target) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&") return queryString } /** * @description 将 search 字符串转换为 query 字符串。 * * @example * ``` * // Expect: "page=1" * const example1 = searchToQueryString("?page=1") * ``` */ export const searchToQueryString = (target: SearchStringLoose): QueryString => { return neatenQueryString(target) } /** * @description 将 search 字符串解析为查询参数对象。 * * @example * ``` * // Expect: { page: "1", size: "20" } * const example1 = searchToQueryObject("?page=1&size=20") * ``` */ export const searchToQueryObject = (target: SearchStringLoose): QueryObject => { return queryStringToQueryObject(searchToQueryString(target)) } /** * @description 将 query 字符串转换为 search 字符串。 * * @example * ``` * // Expect: "?page=1" * const example1 = queryStringToSearch("page=1") * ``` */ export const queryStringToSearch = (target: QueryString): SearchStringStrict => { return neatenSearch(target) } /** * @description 将查询参数对象转换为 search 字符串。 * * @example * ``` * // Expect: "?page=1&size=20" * const example1 = queryObjectToSearch({ page: "1", size: "20" }) * ``` */ export const queryObjectToSearch = (target: QueryObject): SearchStringStrict => { return queryStringToSearch(queryObjectToQueryString(target)) } /** * @description 将查询参数统一转换为 search 字符串。 * * @example * ``` * // Expect: "?page=1" * const example1 = toSearch("page=1") * * // Expect: "?page=1" * const example2 = toSearch({ page: "1" }) * ``` */ export const toSearch = (target: SearchUnion): SearchStringStrict => { if (internalIsString(target)) { return neatenSearch(target) } else if (internalIsObject(target)) { return queryObjectToSearch(target) } else { throw new TypeError('"target" is expected to be type of "string" | "object".') } } /** * @description 将查询参数统一转换为 query 字符串。 * * @example * ``` * // Expect: "page=1" * const example1 = toQueryString("?page=1") * * // Expect: "page=1" * const example2 = toQueryString({ page: "1" }) * ``` */ export const toQueryString = (target: SearchUnion): QueryString => { if (internalIsString(target)) { return neatenQueryString(target) } else if (internalIsObject(target)) { return queryObjectToQueryString(target) } else { throw new TypeError('"target" is expected to be type of "string" | "object".') } } /** * @description 将查询参数统一转换为查询参数对象。 * * @example * ``` * // Expect: { page: "1" } * const example1 = toQueryObject("?page=1") * * // Expect: { page: "1" } * const example2 = toQueryObject({ page: "1" }) * ``` */ export const toQueryObject = (target: SearchUnion): QueryObject => { if (internalIsString(target)) { return queryStringToQueryObject(target) } else if (internalIsObject(target)) { return target } else { throw new TypeError('"target" is expected to be type of "string" | "object".') } } /** * @description 宽松包含关系:查询参数的键集合是目标查询参数键集合的子集。 * * @example * ``` * // Expect: true * const example1 = isSearchLooseIncludes("?a=1&b=2", ["a"]) * * // Expect: false * const example2 = isSearchLooseIncludes("?a=1&b=2", ["a", "c"]) * ``` */ export const isSearchLooseIncludes = (search: SearchUnion, query: string | string[]): boolean => { const existKeys = Object.keys(toQueryObject(search)) const queryKeys = internalIsString(query) ? [query] : query return queryKeys.every((queryKey) => existKeys.includes(queryKey)) } /** * @description 严格包含关系:查询参数的键集合与目标查询参数键集合完全相同。 * * @example * ``` * // Expect: true * const example1 = isSearchStrictIncludes("?a=1&b=2", ["a", "b"]) * * // Expect: false * const example2 = isSearchStrictIncludes("?a=1&b=2", ["a"]) * ``` */ export const isSearchStrictIncludes = (search: SearchUnion, query: string | string[]): boolean => { const existKeys = Object.keys(toQueryObject(search)).toSorted() const queryKeys = (internalIsString(query) ? [query] : query).toSorted() if (existKeys.length !== queryKeys.length) { return false } return existKeys.every((key, index) => key === queryKeys[index]) } /** * @description 包含关系:查询参数的键集合必须包含目标查询参数键集合,并且不能包含除目标查询参数键集合之外的其他键。 * * @example * ``` * // Expect: true * const example1 = isSearchIncludes("?a=1&b=2", { a: "required", b: "optional" }) * * // Expect: false * const example2 = isSearchIncludes("?a=1&b=2", { a: "required" }) * ``` */ export const isSearchIncludes = ( search: SearchUnion, query: Record, ): boolean => { const existKeys = Object.keys(toQueryObject(search)) const requiredKeys = Object.entries(query) .filter(([, value]) => value === "required") .map(([key]) => key) const optionalKeys = Object.entries(query) .filter(([, value]) => value === "optional") .map(([key]) => key) const requiredKeysExist = requiredKeys.every((requiredKey) => existKeys.includes(requiredKey)) const extraKeysExist = existKeys.some( (existKey) => !requiredKeys.includes(existKey) && !optionalKeys.includes(existKey), ) return requiredKeysExist && !extraKeysExist } /** * @description 宽松相等:查询参数的键集合是目标查询参数键集合的子集,并且对应的值相等。 * * @example * ``` * // Expect: true * const example1 = isSearchLooseEqual("?a=1&b=2", "?a=1") * * // Expect: false * const example2 = isSearchLooseEqual("?a=1&b=2", "?a=1&c=3") * ``` */ export const isSearchLooseEqual = (searchA: SearchUnion, searchB: SearchUnion): boolean => { const queryObjectA = toQueryObject(searchA) const queryObjectB = toQueryObject(searchB) return Object.entries(queryObjectB).every(([key, value]) => queryObjectA[key] === value) } /** * @description 严格相等:查询参数的键集合与目标查询参数键集合完全相同,并且对应的值相等。 * * @example * ``` * // Expect: true * const example1 = isSearchStrictEqual("?a=1&b=2", "?b=2&a=1") * * // Expect: false * const example2 = isSearchStrictEqual("?a=1&b=2", "?a=1") * ``` */ export const isSearchStrictEqual = (searchA: SearchUnion, searchB: SearchUnion): boolean => { const queryObjectA = toQueryObject(searchA) const queryObjectB = toQueryObject(searchB) const lengthEqual = Object.keys(queryObjectA).length === Object.keys(queryObjectB).length const valueEqual = Object.entries(queryObjectB).every( ([key, value]) => queryObjectA[key] === value, ) return lengthEqual && valueEqual }