universal-common
Version:
Library that provides useful missing base class library functionality.
314 lines (275 loc) • 9.14 kB
JavaScript
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();
}
}