|
| 1 | +/** |
| 2 | + * @since 4.0.0 |
| 3 | + */ |
| 4 | +import * as Cause from "../../Cause.ts" |
| 5 | +import { dual } from "../../Function.ts" |
| 6 | +import * as Redacted from "../../Redacted.ts" |
| 7 | +import * as Result from "../../Result.ts" |
| 8 | +import * as UrlParams from "./UrlParams.ts" |
| 9 | + |
| 10 | +/** |
| 11 | + * Parses a URL string into a `URL` object, returning an `Result` type for safe |
| 12 | + * error handling. |
| 13 | + * |
| 14 | + * **Details** |
| 15 | + * |
| 16 | + * This function converts a string into a `URL` object, enabling safe URL |
| 17 | + * parsing with built-in error handling. If the string is invalid or fails to |
| 18 | + * parse, this function does not throw an error; instead, it wraps the error in |
| 19 | + * a `IllegalArgumentError` and returns it as the `Failure` value of an |
| 20 | + * `Result`. The `Success` value contains the successfully parsed `URL`. |
| 21 | + * |
| 22 | + * An optional `base` parameter can be provided to resolve relative URLs. If |
| 23 | + * specified, the function interprets the input `url` as relative to this |
| 24 | + * `base`. This is especially useful when dealing with URLs that might not be |
| 25 | + * fully qualified. |
| 26 | + * |
| 27 | + * **Example** |
| 28 | + * |
| 29 | + * ```ts |
| 30 | + * import { Url } from "effect/unstable/http" |
| 31 | + * import { Result } from "effect" |
| 32 | + * |
| 33 | + * // Parse an absolute URL |
| 34 | + * // |
| 35 | + * // ┌─── Result<URL, IllegalArgumentError> |
| 36 | + * // ▼ |
| 37 | + * const parsed = Url.fromString("https://example.com/path") |
| 38 | + * |
| 39 | + * if (Result.isSuccess(parsed)) { |
| 40 | + * console.log("Parsed URL:", parsed.success.toString()) |
| 41 | + * } else { |
| 42 | + * console.log("Error:", parsed.failure.message) |
| 43 | + * } |
| 44 | + * // Output: Parsed URL: https://example.com/path |
| 45 | + * |
| 46 | + * // Parse a relative URL with a base |
| 47 | + * const relativeParsed = Url.fromString("/relative-path", "https://example.com") |
| 48 | + * |
| 49 | + * if (Result.isSuccess(relativeParsed)) { |
| 50 | + * console.log("Parsed relative URL:", relativeParsed.success.toString()) |
| 51 | + * } else { |
| 52 | + * console.log("Error:", relativeParsed.failure.message) |
| 53 | + * } |
| 54 | + * // Output: Parsed relative URL: https://example.com/relative-path |
| 55 | + * ``` |
| 56 | + * |
| 57 | + * @since 4.0.0 |
| 58 | + * @category Constructors |
| 59 | + */ |
| 60 | +export const fromString: { |
| 61 | + (url: string, base?: string | URL | undefined): Result.Result<URL, Cause.IllegalArgumentError> |
| 62 | +} = (url, base) => |
| 63 | + Result.try({ |
| 64 | + try: () => new URL(url, base), |
| 65 | + catch: () => |
| 66 | + new Cause.IllegalArgumentError(`Invalid URL: "${url}"${base !== undefined ? ` with base "${base}"` : ""}`) |
| 67 | + }) |
| 68 | + |
| 69 | +/** |
| 70 | + * This function clones the original `URL` object and applies a callback to the |
| 71 | + * clone, allowing multiple updates at once. |
| 72 | + * |
| 73 | + * **Example** |
| 74 | + * |
| 75 | + * ```ts |
| 76 | + * import { Url } from "effect/unstable/http" |
| 77 | + * |
| 78 | + * const myUrl = new URL("https://example.com") |
| 79 | + * |
| 80 | + * const mutatedUrl = Url.mutate(myUrl, (url) => { |
| 81 | + * url.username = "user" |
| 82 | + * url.password = "pass" |
| 83 | + * }) |
| 84 | + * |
| 85 | + * console.log("Mutated:", mutatedUrl.toString()) |
| 86 | + * // Output: Mutated: https://user:pass@example.com/ |
| 87 | + * ``` |
| 88 | + * |
| 89 | + * @since 4.0.0 |
| 90 | + * @category Modifiers |
| 91 | + */ |
| 92 | +export const mutate: { |
| 93 | + (f: (url: URL) => void): (self: URL) => URL |
| 94 | + (self: URL, f: (url: URL) => void): URL |
| 95 | +} = dual(2, (self: URL, f: (url: URL) => void) => { |
| 96 | + const copy = new URL(self) |
| 97 | + f(copy) |
| 98 | + return copy |
| 99 | +}) |
| 100 | + |
| 101 | +/** @internal */ |
| 102 | +const immutableURLSetter = <P extends keyof URL, A = never>(property: P): { |
| 103 | + (value: URL[P] | A): (url: URL) => URL |
| 104 | + (url: URL, value: URL[P] | A): URL |
| 105 | +} => |
| 106 | + dual(2, (url: URL, value: URL[P]) => |
| 107 | + mutate(url, (url) => { |
| 108 | + url[property] = value |
| 109 | + })) |
| 110 | + |
| 111 | +/** |
| 112 | + * Updates the hash fragment of the URL. |
| 113 | + * |
| 114 | + * @since 4.0.0 |
| 115 | + * @category Setters |
| 116 | + */ |
| 117 | +export const setHash: { |
| 118 | + (hash: string): (url: URL) => URL |
| 119 | + (url: URL, hash: string): URL |
| 120 | +} = immutableURLSetter("hash") |
| 121 | + |
| 122 | +/** |
| 123 | + * Updates the host (domain and port) of the URL. |
| 124 | + * |
| 125 | + * @since 4.0.0 |
| 126 | + * @category Setters |
| 127 | + */ |
| 128 | +export const setHost: { |
| 129 | + (host: string): (url: URL) => URL |
| 130 | + (url: URL, host: string): URL |
| 131 | +} = immutableURLSetter("host") |
| 132 | + |
| 133 | +/** |
| 134 | + * Updates the domain of the URL without modifying the port. |
| 135 | + * |
| 136 | + * @since 4.0.0 |
| 137 | + * @category Setters |
| 138 | + */ |
| 139 | +export const setHostname: { |
| 140 | + (hostname: string): (url: URL) => URL |
| 141 | + (url: URL, hostname: string): URL |
| 142 | +} = immutableURLSetter("hostname") |
| 143 | + |
| 144 | +/** |
| 145 | + * Replaces the entire URL string. |
| 146 | + * |
| 147 | + * @since 4.0.0 |
| 148 | + * @category Setters |
| 149 | + */ |
| 150 | +export const setHref: { |
| 151 | + (href: string): (url: URL) => URL |
| 152 | + (url: URL, href: string): URL |
| 153 | +} = immutableURLSetter("href") |
| 154 | + |
| 155 | +/** |
| 156 | + * Updates the password used for authentication. |
| 157 | + * |
| 158 | + * @since 4.0.0 |
| 159 | + * @category Setters |
| 160 | + */ |
| 161 | +export const setPassword: { |
| 162 | + (password: string | Redacted.Redacted): (url: URL) => URL |
| 163 | + (url: URL, password: string | Redacted.Redacted): URL |
| 164 | +} = dual(2, (url: URL, password: string | Redacted.Redacted) => |
| 165 | + mutate(url, (url) => { |
| 166 | + url.password = typeof password === "string" |
| 167 | + ? password : |
| 168 | + Redacted.value(password) |
| 169 | + })) |
| 170 | + |
| 171 | +/** |
| 172 | + * Updates the path of the URL. |
| 173 | + * |
| 174 | + * @since 4.0.0 |
| 175 | + * @category Setters |
| 176 | + */ |
| 177 | +export const setPathname: { |
| 178 | + (pathname: string): (url: URL) => URL |
| 179 | + (url: URL, pathname: string): URL |
| 180 | +} = immutableURLSetter("pathname") |
| 181 | + |
| 182 | +/** |
| 183 | + * Updates the port of the URL. |
| 184 | + * |
| 185 | + * @since 4.0.0 |
| 186 | + * @category Setters |
| 187 | + */ |
| 188 | +export const setPort: { |
| 189 | + (port: string | number): (url: URL) => URL |
| 190 | + (url: URL, port: string | number): URL |
| 191 | +} = immutableURLSetter("port") |
| 192 | + |
| 193 | +/** |
| 194 | + * Updates the protocol (e.g., `http`, `https`). |
| 195 | + * |
| 196 | + * @since 4.0.0 |
| 197 | + * @category Setters |
| 198 | + */ |
| 199 | +export const setProtocol: { |
| 200 | + (protocol: string): (url: URL) => URL |
| 201 | + (url: URL, protocol: string): URL |
| 202 | +} = immutableURLSetter("protocol") |
| 203 | + |
| 204 | +/** |
| 205 | + * Updates the query string of the URL. |
| 206 | + * |
| 207 | + * @since 4.0.0 |
| 208 | + * @category Setters |
| 209 | + */ |
| 210 | +export const setSearch: { |
| 211 | + (search: string): (url: URL) => URL |
| 212 | + (url: URL, search: string): URL |
| 213 | +} = immutableURLSetter("search") |
| 214 | + |
| 215 | +/** |
| 216 | + * Updates the username used for authentication. |
| 217 | + * |
| 218 | + * @since 4.0.0 |
| 219 | + * @category Setters |
| 220 | + */ |
| 221 | +export const setUsername: { |
| 222 | + (username: string): (url: URL) => URL |
| 223 | + (url: URL, username: string): URL |
| 224 | +} = immutableURLSetter("username") |
| 225 | + |
| 226 | +/** |
| 227 | + * Updates the query parameters of a URL. |
| 228 | + * |
| 229 | + * **Details** |
| 230 | + * |
| 231 | + * This function allows you to set or replace the query parameters of a `URL` |
| 232 | + * object using the provided `UrlParams`. It creates a new `URL` object with the |
| 233 | + * updated parameters, leaving the original object unchanged. |
| 234 | + * |
| 235 | + * **Example** |
| 236 | + * |
| 237 | + * ```ts |
| 238 | + * import { Url, UrlParams } from "effect/unstable/http" |
| 239 | + * |
| 240 | + * const myUrl = new URL("https://example.com?foo=bar") |
| 241 | + * |
| 242 | + * // Write parameters |
| 243 | + * const updatedUrl = Url.setUrlParams( |
| 244 | + * myUrl, |
| 245 | + * UrlParams.fromInput([["key", "value"]]) |
| 246 | + * ) |
| 247 | + * |
| 248 | + * console.log(updatedUrl.toString()) |
| 249 | + * // Output: https://example.com/?key=value |
| 250 | + * ``` |
| 251 | + * |
| 252 | + * @since 4.0.0 |
| 253 | + * @category Setters |
| 254 | + */ |
| 255 | +export const setUrlParams: { |
| 256 | + (urlParams: UrlParams.UrlParams): (url: URL) => URL |
| 257 | + (url: URL, urlParams: UrlParams.UrlParams): URL |
| 258 | +} = dual(2, (url: URL, searchParams: UrlParams.UrlParams) => |
| 259 | + mutate(url, (url) => { |
| 260 | + url.search = UrlParams.toString(searchParams) |
| 261 | + })) |
| 262 | + |
| 263 | +/** |
| 264 | + * Retrieves the query parameters from a URL. |
| 265 | + * |
| 266 | + * **Details** |
| 267 | + * |
| 268 | + * This function extracts the query parameters from a `URL` object and returns |
| 269 | + * them as `UrlParams`. The resulting structure can be easily manipulated or |
| 270 | + * inspected. |
| 271 | + * |
| 272 | + * **Example** |
| 273 | + * |
| 274 | + * ```ts |
| 275 | + * import { Url } from "effect/unstable/http" |
| 276 | + * |
| 277 | + * const myUrl = new URL("https://example.com?foo=bar") |
| 278 | + * |
| 279 | + * // Read parameters |
| 280 | + * const params = Url.urlParams(myUrl) |
| 281 | + * |
| 282 | + * console.log(params) |
| 283 | + * // Output: [ [ 'foo', 'bar' ] ] |
| 284 | + * ``` |
| 285 | + * |
| 286 | + * @since 4.0.0 |
| 287 | + * @category Getters |
| 288 | + */ |
| 289 | +export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams) |
| 290 | + |
| 291 | +/** |
| 292 | + * Reads, modifies, and updates the query parameters of a URL. |
| 293 | + * |
| 294 | + * **Details** |
| 295 | + * |
| 296 | + * This function provides a functional way to interact with query parameters by |
| 297 | + * reading the current parameters, applying a transformation function, and then |
| 298 | + * writing the updated parameters back to the URL. It returns a new `URL` object |
| 299 | + * with the modified parameters, ensuring immutability. |
| 300 | + * |
| 301 | + * **Example** |
| 302 | + * |
| 303 | + * ```ts |
| 304 | + * import { Url, UrlParams } from "effect/unstable/http" |
| 305 | + * |
| 306 | + * const myUrl = new URL("https://example.com?foo=bar") |
| 307 | + * |
| 308 | + * const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) |
| 309 | + * |
| 310 | + * console.log(changedUrl.toString()) |
| 311 | + * // Output: https://example.com/?foo=bar&key=value |
| 312 | + * ``` |
| 313 | + * |
| 314 | + * @since 4.0.0 |
| 315 | + * @category Modifiers |
| 316 | + */ |
| 317 | +export const modifyUrlParams: { |
| 318 | + (f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): (url: URL) => URL |
| 319 | + (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams): URL |
| 320 | +} = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.UrlParams) => |
| 321 | + mutate(url, (url) => { |
| 322 | + const params = f(UrlParams.fromInput(url.searchParams)) |
| 323 | + url.search = UrlParams.toString(params) |
| 324 | + })) |
0 commit comments