UNPKG

universal-common

Version:

Library that provides useful missing base class library functionality.

314 lines (275 loc) 9.14 kB
import ArgumentError from "./ArgumentError.js"; import InvalidOperationError from "./InvalidOperationError.js"; import StringBuilder from "./StringBuilder.js"; /** * A builder class for constructing and manipulating URIs (Uniform Resource Identifiers). * Follows the builder pattern to allow fluent chaining of operations. * * This class provides methods to: * - Set and manipulate URI components (scheme, host, path, query parameters, etc.) * - Build complete URI strings * - Parse existing URIs into modifiable components */ export default class UriBuilder { // Private fields for URI components #scheme; // The URI scheme (e.g., "http", "https", "ftp") #host; // The hostname (e.g., "example.com") #username; // Optional username for authentication #password; // Optional password for authentication #port; // Optional port number #pathSegments = []; // Array of path segments #queryParameters = new URLSearchParams(); // URL query parameters /** * Creates a new UriBuilder instance. * * @overload * @description Creates an empty UriBuilder * * @overload * @param {string} uri - An existing URI to parse and use as a starting point * * @overload * @param {string} scheme - The URI scheme (e.g., "http", "https") * @param {string} host - The hostname (e.g., "example.com") * * @overload * @param {string} scheme - The URI scheme * @param {string} host - The hostname * @param {number|string} port - The port number * * @throws {ArgumentError} If invalid arguments are provided */ constructor() { // Constructor overload #1: Empty constructor if (arguments.length == 0) { } // Constructor overload #2: Parse from existing URI else if (arguments.length == 1) { let url = new URL(arguments[0]); this.#scheme = url.protocol.slice(0, -1); this.#host = url.hostname; if (url.username) { this.#username = url.username; } if (url.password) { this.#password = url.password; } if (url.port) { this.#port = parseInt(url.port); } this.#pathSegments = url.pathname.split("/").filter(x => x != null && x != ""); for (let entry of url.searchParams.entries()) { this.addQuery(entry[0], entry[1]); } } // Constructor overload #3: Scheme and host else if (arguments.length == 2) { this.#scheme = arguments[0]; this.#host = arguments[1]; } // Constructor overload #4: Scheme, host, and port else if (arguments.length == 3) { this.#scheme = arguments[0]; this.#host = arguments[1]; if (typeof (arguments[2]) == typeof (Number)) { this.#port = arguments[2]; } else { this.#port = parseInt(arguments[2]); } } else { throw new ArgumentError("Expected 0-3 arguments."); } } /** * Gets the URI scheme. * @returns {string?} The scheme component */ get scheme() { return this.#scheme; } /** * Sets the URI scheme. * @param {string} value - The scheme to set */ set scheme(value) { this.#scheme = value; } /** * Gets the hostname. * @returns {string?} The host component */ get host() { return this.#host; } /** * Sets the hostname. * @param {string} value - The hostname to set */ set host(value) { this.#host = value; } /** * Gets the username for authentication. * @returns {string?} The username component */ get username() { return this.#username; } /** * Sets the username for authentication. * @param {string} value - The username to set */ set username(value) { this.#username = value; } /** * Gets the password for authentication. * @returns {string?} The password component */ get password() { return this.#password; } /** * Sets the password for authentication. * @param {string} value - The password to set */ set password(value) { this.#password = value; } /** * Gets the port number. * @returns {number?} The port component */ get port() { return this.#port; } /** * Sets the port number. * @param {number|string} value - The port to set */ set port(value) { this.#port = value; } /** * Gets the complete path as a string. * @returns {string} The path component with leading and trailing slashes */ get path() { let stringBuilder = new StringBuilder(); stringBuilder.append("/"); for (let segment of this.#pathSegments) { stringBuilder.append(`${segment}/`); } return stringBuilder.toString(); } /** * Gets the query string. * @returns {string} The query component as a string */ get query() { return this.#queryParameters.toString(); } /** * Gets a copy of the path segments array. * @returns {Array<string>} Array of path segments */ get segments() { return [...this.#pathSegments]; } /** * Adds a single path segment. * @param {string} value - The path segment to add * @returns {UriBuilder} This builder instance for method chaining */ addSegment(value) { this.#pathSegments.push(value); return this; } /** * Adds multiple path segments. * Accepts either an array of segments or multiple segment arguments. * * @param {...string|Array<string>} segments - Path segments to add * @returns {UriBuilder} This builder instance for method chaining * * Usage examples: * builder.addSegments("api", "v1", "users"); * builder.addSegments(["api", "v1", "users"]); */ addSegments() { const segments = Array.isArray(arguments[0]) && arguments.length === 1 ? arguments[0] // Use the array directly : arguments; // Use the arguments object for (let segment of segments) { this.addSegment(segment); } return this; } /** * Adds a query parameter. * @param {string} name - The parameter name * @param {string} value - The parameter value * @returns {UriBuilder} This builder instance for method chaining */ addQuery(name, value) { this.#queryParameters.append(name, value); return this; } /** * Adds multiple query parameters from an object or Map. * @param {Object|Map} queries - Object or Map containing name-value pairs * @returns {UriBuilder} This builder instance for method chaining * * Usage examples: * builder.addQueries({ page: 1, limit: 10 }); * builder.addQueries(new Map([["page", 1], ["limit", 10]])); */ addQueries(queries) { if (queries instanceof Map) { for (const [key, value] of queries) { this.addQuery(key, value); } } else { for (const key in queries) { this.addQuery(key, queries[key]); } } return this; } /** * Builds and returns the complete URI string. * @returns {string} The complete URI * @throws {Error} If scheme or host are not specified */ get uri() { // Validate required components if (!this.scheme || !this.host) { throw new InvalidOperationError("At least scheme and host must be specified."); } let stringBuilder = new StringBuilder(`${this.scheme}://`); // Add authentication if provided if (this.username || this.password) { if (this.username) { stringBuilder.append(this.username); } if (this.password) { stringBuilder.append(`:${this.password}`); } stringBuilder.append("@"); } // Add host and port stringBuilder.append(this.host); if (this.port) { stringBuilder.append(`:${this.port}`); } // Add path (removing the trailing slash) stringBuilder.append(this.path.slice(0, -1)); // Add query string if present if (this.query) { stringBuilder.append(`?${this.query}`); } return stringBuilder.toString(); } }