All files utilities.ts

100% Statements 53/53
100% Branches 59/59
100% Functions 4/4
100% Lines 53/53

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111                                      1x 124x 124x   30x 30x 30x 30x 14x 8x 124x 124x   21x 21x 21x 21x 21x 7x 124x 124x   53x 53x 53x 53x 53x 18x 124x 124x                                 1x 117x   36x 36x 64x   64x   48x 36x 36x   117x   117x 16x 16x 16x   16x 16x 16x                 1x 104x 104x 7x 7x           1x 33x 33x 33x  
import type { Options } from "./types";
 
/**
 * Replaces path parameters in the given path with actual values from params.
 *
 * @param path - The endpoint path, which can include path parameters. (e.g., "/users/[id]")
 * @param params - An object containing the values for the path parameters. (e.g., { id: "1" })
 * @param mocking - If true, missing parameters are replaced with colon syntax (e.g., ":id") instead of throwing an error, and the output will not be URL-encoded.
 *
 * @throws Error - Will throw an error if a required parameter is missing or if the parameter type is incorrect.
 * @returns The path with encoded path parameters replaced with actual values. (e.g., "/users/1")
 *
 * @example
 * replacePathParams("/users/[id]", { id: "1" });
 * // => "/users/1"
 *
 * replacePathParams("/posts/[[...slug]]", { slug: ["hoge", "fuga"] });
 * // => "/posts/hoge/fuga"
 */
export function replacePathParams(path: string, params: Options["params"], mocking = false): string {
  return path
    .replace(/\/\[\[\.\.\.(.+?)]]/g, (_, key) => {
      // 1. /[[...slug]] - Optional Catch-all Parameters
      const values = params?.[key];
      if (mocking && values === undefined) return `/:${key}*`;
      if (values === undefined) return "";
      if (!Array.isArray(values)) throw new Error(`"${key}" must be an array`);
      if (mocking) return `/${values.join("/")}`;
      return `/${values.map(encodeURIComponent).join("/")}`;
    })
    .replace(/\[\.\.\.(.+?)]/g, (_, key) => {
      // 2. [...slug] - Catch-all Parameters
      const values = params?.[key];
      if (mocking && values === undefined) return `:${key}+`;
      if (values === undefined) throw new Error(`"${key}" is required`);
      if (!Array.isArray(values)) throw new Error(`"${key}" must be an array`);
      if (mocking) return values.join("/");
      return values.map(encodeURIComponent).join("/");
    })
    .replace(/\[(.+?)]/g, (_, key) => {
      // 3. [slug] - Path Parameter
      const value = params?.[key];
      if (mocking && value === undefined) return `:${key}`;
      if (value === undefined) throw new Error(`"${key}" is required`);
      if (Array.isArray(value)) throw new Error(`"${key}" must not be an array`);
      if (mocking) return String(value);
      return encodeURIComponent(String(value));
    });
}
 
/**
 * Converts an object of query parameters into a URL-encoded query string.
 * Handles array values by repeating the key for each value.
 *
 * @param queries - An object of query parameters.
 * @param mocking - If true, the query string will not be URL-encoded and will use a simple key=value format.
 * @returns A URL-encoded and sorted query string.
 *
 * @example
 * stringifyQueries({ z: "last", a: "first", n: "middle" });
 * // => "?a=first&n=middle&z=last"
 *
 * stringifyQueries({ search: ["apple", "banana"], page: 2 });
 * // => "?page=2&search=apple&search=banana"
 */
export function stringifyQueries(queries: Options["queries"], mocking = false): string {
  if (!queries || Object.keys(queries).length === 0) return "";
 
  const entries = Object.entries(queries)
    .flatMap(([key, value]) => {
      if (value === null || value === undefined) return [];
 
      if (Array.isArray(value)) return value.filter((v) => v !== null && v !== undefined).map((v) => [key, String(v)]);
 
      return [[key, String(value)]];
    })
    .sort(([a], [b]) => a.localeCompare(b));
 
  if (entries.length === 0) return "";
 
  if (mocking) {
    const queryString = entries.map(([key, value]) => `${key}=${value}`).join("&");
    return `?${queryString}`;
  }
 
  const searchParams = new URLSearchParams(entries).toString().replace(/\+/g, "%20");
  return `?${searchParams}`;
}
 
/**
 * Converts a hash value into a string representation.
 *
 * @param hash - The hash value to be stringified.
 * @param mocking - A flag indicating whether to mock the hash value.
 * @return The stringified hash value.
 */
export function stringifyHash(hash: Options["hash"], mocking = false): string {
  if (!hash) return "";
  if (mocking) return `#${hash}`;
  return `#${encodeURIComponent(String(hash))}`;
}
 
/**
 * Type guard to check if a key is one of the keys in the Options type.
 * @param key - The key to check.
 */
export function isOptions(key: string): key is keyof Options {
  const optionsKeys: (keyof Options)[] = ["params", "queries", "hash"];
  return optionsKeys.includes(key as (typeof optionsKeys)[number]);
}