All files utilities.ts

100% Statements 62/62
100% Branches 65/65
100% Functions 6/6
100% Lines 62/62

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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152                                      1x 139x 139x   31x 31x 31x 31x 14x 8x 139x 139x   21x 21x 21x 21x 21x 7x 139x 139x   54x 54x 54x 54x 54x 19x 139x 139x                                   1x 122x 122x 122x 122x 122x                               1x 118x 118x                                 1x 134x   40x 40x 68x   68x   52x 40x 40x   134x   134x 16x 16x 16x   20x 20x 20x                 1x 119x 119x 7x 7x           1x 37x 37x 37x  
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));
    });
}
 
/**
 * Collapses repeated slashes and removes a trailing slash from the path, but preserves `://` separators.
 *
 * @param path - The path to trim. (e.g., "/users//1/")
 * @returns The trimmed path. (e.g., "/users/1")
 *
 * @example
 * trimPath("/users//1/");
 * // => "/users/1"
 *
 * trimPath("/");
 * // => ""
 *
 * trimPath("myapp://path/");
 * // => "myapp://path"
 */
export function trimPath(path: string): string {
  return path
    .split("://")
    .map((segment) => segment.replace(/\/{2,}/g, "/").replace(/\/$/, ""))
    .join("://");
}
 
/**
 * Prepends the base URL to the path, falling back to "/" when both are empty.
 *
 * @param baseUrl - The base URL to prepend. (e.g., "https://example.com")
 * @param path - The path to append. (e.g., "/users")
 * @returns The joined URL. (e.g., "https://example.com/users")
 *
 * @example
 * joinBaseUrl("https://example.com", "");
 * // => "https://example.com"
 *
 * joinBaseUrl("", "");
 * // => "/"
 */
export function joinBaseUrl(baseUrl: string, path: string): string {
  return `${baseUrl}${path}` || "/";
}
 
/**
 * 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]);
}