/** * Parses and edits platform `URL` values. * * The HTTP modules use the standard `URL` object as their URL representation. * This module adds safe parsing and helpers that return updated copies when * changing credentials, host, path, protocol, query, or hash parts. Query * strings can also be read or updated through `UrlParams`. * * @stability unstable * @since 4.0.0 */ import * as Cause from "../Cause.ts" import * as Data from "../Data.ts" import { dual } from "../Function.ts" import * as Redacted from "../Redacted.ts" import * as Result from "../Result.ts" import * as internal from "./internal/urlParams.ts" import * as UrlParams from "./UrlParams.ts" /** * Error returned when constructing a `URL` fails. * * @stability unstable * @category errors * @since 4.0.0 */ export class UrlError extends Data.TaggedError("UrlError")<{ readonly cause: unknown }> {} /** * Creates a `URL` safely by appending `UrlParams` and an optional hash to a URL string. * * **Details** * * Returns a `Result` that fails with `UrlError` if the URL cannot be constructed. * * @stability unstable * @category constructors * @since 4.0.0 */ export const make = ( url: string, params: UrlParams.UrlParams, hash: string | undefined ): Result.Result => Result.try({ try: () => { const urlInstance = new URL(url, baseUrl()) for (let i = 0; i < params.params.length; i++) { const [key, value] = params.params[i] if (value !== undefined) { urlInstance.searchParams.append(key, internal.unwrap(value)) } } if (hash !== undefined) { urlInstance.hash = hash } return urlInstance }, catch: (cause) => new UrlError({ cause }) }) const baseUrl = (): string | undefined => { if ( "location" in globalThis && globalThis.location !== undefined && globalThis.location.origin !== undefined && globalThis.location.pathname !== undefined ) { const base = location.origin + location.pathname try { return new URL(base).href } catch { return undefined } } return undefined } /** * Parses a URL string safely into a `URL` object, returning a `Result` type for * error handling. * * **Details** * * This function converts a string into a `URL` object, enabling safe URL * parsing with built-in error handling. If the string is invalid or fails to * parse, this function does not throw an error; instead, it wraps the error in * a `IllegalArgumentError` and returns it as the `Failure` value of an * `Result`. The `Success` value contains the successfully parsed `URL`. * * An optional `base` parameter can be provided to resolve relative URLs. If * specified, the function interprets the input `url` as relative to this * `base`. This is especially useful when dealing with URLs that might not be * fully qualified. * * **Example** (Parsing absolute and relative URLs) * * ```ts import.meta.vitest * import { Result } from "effect" * import { Url } from "effect/http" * * // Parse an absolute URL * // * // ┌─── Result * // ▼ * const parsed = Url.fromString("https://example.com/path") * * Result.map(parsed, (url) => url.toString()) // => Result.succeed("https://example.com/path") * * // Parse a relative URL with a base * const relativeParsed = Url.fromString("/relative-path", "https://example.com") * * Result.map(relativeParsed, (url) => url.toString()) // => Result.succeed("https://example.com/relative-path") * ``` * * @stability unstable * @category constructors * @since 4.0.0 */ export const fromString: { /** * Parses a URL string safely into a `URL` object, returning a `Result` type for * error handling. * * **Details** * * This function converts a string into a `URL` object, enabling safe URL * parsing with built-in error handling. If the string is invalid or fails to * parse, this function does not throw an error; instead, it wraps the error in * a `IllegalArgumentError` and returns it as the `Failure` value of an * `Result`. The `Success` value contains the successfully parsed `URL`. * * An optional `base` parameter can be provided to resolve relative URLs. If * specified, the function interprets the input `url` as relative to this * `base`. This is especially useful when dealing with URLs that might not be * fully qualified. * * **Example** (Parsing absolute and relative URLs) * * ```ts import.meta.vitest * import { Result } from "effect" * import { Url } from "effect/http" * * // Parse an absolute URL * // * // ┌─── Result * // ▼ * const parsed = Url.fromString("https://example.com/path") * * Result.map(parsed, (url) => url.toString()) // => Result.succeed("https://example.com/path") * * // Parse a relative URL with a base * const relativeParsed = Url.fromString("/relative-path", "https://example.com") * * Result.map(relativeParsed, (url) => url.toString()) // => Result.succeed("https://example.com/relative-path") * ``` * * @stability unstable * @category constructors * @since 4.0.0 */ (url: string, base?: string | URL | undefined): Result.Result } = (url, base) => Result.try({ try: () => new URL(url, base), catch: () => new Cause.IllegalArgumentError(`Invalid URL: "${url}"${base !== undefined ? ` with base "${base}"` : ""}`) }) /** * Updates a cloned `URL` with a callback, allowing multiple changes at once. * * **Example** (Mutating URL credentials) * * ```ts import.meta.vitest * import { Url } from "effect/http" * * const myUrl = new URL("https://example.com") * * const mutatedUrl = Url.mutate(myUrl, (url) => { * url.username = "user" * url.password = "pass" * }) * * mutatedUrl.toString() // => "https://user:pass@example.com/" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ export const mutate: { /** * Updates a cloned `URL` with a callback, allowing multiple changes at once. * * **Example** (Mutating URL credentials) * * ```ts import.meta.vitest * import { Url } from "effect/http" * * const myUrl = new URL("https://example.com") * * const mutatedUrl = Url.mutate(myUrl, (url) => { * url.username = "user" * url.password = "pass" * }) * * mutatedUrl.toString() // => "https://user:pass@example.com/" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ (f: (url: URL) => void): (self: URL) => URL /** * Updates a cloned `URL` with a callback, allowing multiple changes at once. * * **Example** (Mutating URL credentials) * * ```ts import.meta.vitest * import { Url } from "effect/http" * * const myUrl = new URL("https://example.com") * * const mutatedUrl = Url.mutate(myUrl, (url) => { * url.username = "user" * url.password = "pass" * }) * * mutatedUrl.toString() // => "https://user:pass@example.com/" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ (self: URL, f: (url: URL) => void): URL } = dual(2, (self: URL, f: (url: URL) => void) => { const copy = new URL(self) f(copy) return copy }) /** * @internal */ const immutableURLSetter =

(property: P): { (value: URL[P] | A): (url: URL) => URL (url: URL, value: URL[P] | A): URL } => dual(2, (url: URL, value: URL[P]) => mutate(url, (url) => { url[property] = value })) /** * Updates the hash fragment of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ export const setHash: { /** * Updates the hash fragment of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (hash: string): (url: URL) => URL /** * Updates the hash fragment of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, hash: string): URL } = immutableURLSetter("hash") /** * Updates the host (domain and port) of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ export const setHost: { /** * Updates the host (domain and port) of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (host: string): (url: URL) => URL /** * Updates the host (domain and port) of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, host: string): URL } = immutableURLSetter("host") /** * Updates the domain of the URL without modifying the port. * * @stability unstable * @category setters * @since 4.0.0 */ export const setHostname: { /** * Updates the domain of the URL without modifying the port. * * @stability unstable * @category setters * @since 4.0.0 */ (hostname: string): (url: URL) => URL /** * Updates the domain of the URL without modifying the port. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, hostname: string): URL } = immutableURLSetter("hostname") /** * Replaces the entire URL string. * * @stability unstable * @category setters * @since 4.0.0 */ export const setHref: { /** * Replaces the entire URL string. * * @stability unstable * @category setters * @since 4.0.0 */ (href: string): (url: URL) => URL /** * Replaces the entire URL string. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, href: string): URL } = immutableURLSetter("href") /** * Updates the password used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ export const setPassword: { /** * Updates the password used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ (password: string | Redacted.Redacted): (url: URL) => URL /** * Updates the password used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, password: string | Redacted.Redacted): URL } = dual(2, (url: URL, password: string | Redacted.Redacted) => mutate(url, (url) => { url.password = typeof password === "string" ? password : Redacted.value(password) })) /** * Updates the path of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ export const setPathname: { /** * Updates the path of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (pathname: string): (url: URL) => URL /** * Updates the path of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, pathname: string): URL } = immutableURLSetter("pathname") /** * Updates the port of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ export const setPort: { /** * Updates the port of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (port: string | number): (url: URL) => URL /** * Updates the port of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, port: string | number): URL } = immutableURLSetter("port") /** * Updates the protocol (e.g., `http`, `https`). * * @stability unstable * @category setters * @since 4.0.0 */ export const setProtocol: { /** * Updates the protocol (e.g., `http`, `https`). * * @stability unstable * @category setters * @since 4.0.0 */ (protocol: string): (url: URL) => URL /** * Updates the protocol (e.g., `http`, `https`). * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, protocol: string): URL } = immutableURLSetter("protocol") /** * Updates the query string of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ export const setSearch: { /** * Updates the query string of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (search: string): (url: URL) => URL /** * Updates the query string of the URL. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, search: string): URL } = immutableURLSetter("search") /** * Updates the username used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ export const setUsername: { /** * Updates the username used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ (username: string): (url: URL) => URL /** * Updates the username used for authentication. * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, username: string): URL } = immutableURLSetter("username") /** * Updates the query parameters of a URL. * * **Details** * * This function allows you to set or replace the query parameters of a `URL` * object using the provided `UrlParams`. It creates a new `URL` object with the * updated parameters, leaving the original object unchanged. * * **Example** (Replacing query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * // Write parameters * const updatedUrl = Url.setUrlParams( * myUrl, * UrlParams.fromInput([["key", "value"]]) * ) * * updatedUrl.toString() // => "https://example.com/?key=value" * ``` * * @stability unstable * @category setters * @since 4.0.0 */ export const setUrlParams: { /** * Updates the query parameters of a URL. * * **Details** * * This function allows you to set or replace the query parameters of a `URL` * object using the provided `UrlParams`. It creates a new `URL` object with the * updated parameters, leaving the original object unchanged. * * **Example** (Replacing query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * // Write parameters * const updatedUrl = Url.setUrlParams( * myUrl, * UrlParams.fromInput([["key", "value"]]) * ) * * updatedUrl.toString() // => "https://example.com/?key=value" * ``` * * @stability unstable * @category setters * @since 4.0.0 */ (urlParams: UrlParams.Input): (url: URL) => URL /** * Updates the query parameters of a URL. * * **Details** * * This function allows you to set or replace the query parameters of a `URL` * object using the provided `UrlParams`. It creates a new `URL` object with the * updated parameters, leaving the original object unchanged. * * **Example** (Replacing query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * // Write parameters * const updatedUrl = Url.setUrlParams( * myUrl, * UrlParams.fromInput([["key", "value"]]) * ) * * updatedUrl.toString() // => "https://example.com/?key=value" * ``` * * @stability unstable * @category setters * @since 4.0.0 */ (url: URL, urlParams: UrlParams.Input): URL } = dual(2, (url: URL, urlParams: UrlParams.Input) => mutate(url, (url) => { url.search = UrlParams.toString(UrlParams.fromInput(urlParams)) })) /** * Retrieves the query parameters from a URL. * * **Details** * * This function extracts the query parameters from a `URL` object and returns * them as `UrlParams`. The resulting structure can be easily manipulated or * inspected. * * **Example** (Reading query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * // Read parameters * const params = Url.urlParams(myUrl) * * UrlParams.toString(params) // => "foo=bar" * ``` * * @stability unstable * @category getters * @since 4.0.0 */ export const urlParams = (url: URL): UrlParams.UrlParams => UrlParams.fromInput(url.searchParams) /** * Reads the query parameters of a URL, modifies them, and updates the URL. * * **Details** * * This function provides a functional way to interact with query parameters by * reading the current parameters, applying a transformation function, and then * writing the updated parameters back to the URL. It returns a new `URL` object * with the modified parameters, ensuring immutability. * * **Example** (Modifying query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) * * changedUrl.toString() // => "https://example.com/?foo=bar&key=value" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ export const modifyUrlParams: { /** * Reads the query parameters of a URL, modifies them, and updates the URL. * * **Details** * * This function provides a functional way to interact with query parameters by * reading the current parameters, applying a transformation function, and then * writing the updated parameters back to the URL. It returns a new `URL` object * with the modified parameters, ensuring immutability. * * **Example** (Modifying query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) * * changedUrl.toString() // => "https://example.com/?foo=bar&key=value" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ (f: (urlParams: UrlParams.UrlParams) => UrlParams.Input): (url: URL) => URL /** * Reads the query parameters of a URL, modifies them, and updates the URL. * * **Details** * * This function provides a functional way to interact with query parameters by * reading the current parameters, applying a transformation function, and then * writing the updated parameters back to the URL. It returns a new `URL` object * with the modified parameters, ensuring immutability. * * **Example** (Modifying query parameters) * * ```ts import.meta.vitest * import { Url, UrlParams } from "effect/http" * * const myUrl = new URL("https://example.com?foo=bar") * * const changedUrl = Url.modifyUrlParams(myUrl, UrlParams.append("key", "value")) * * changedUrl.toString() // => "https://example.com/?foo=bar&key=value" * ``` * * @stability unstable * @category transforming * @since 4.0.0 */ (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.Input): URL } = dual(2, (url: URL, f: (urlParams: UrlParams.UrlParams) => UrlParams.Input) => mutate(url, (url) => { const params = f(UrlParams.fromInput(url.searchParams)) url.search = UrlParams.toString(params) }))