/** * Domain Value Object * * Represents a validated and normalized domain name. * Value objects are immutable and enforce validation rules. * * Domain invariants enforced: * - Domain must be valid format * - Domain is normalized (lowercase, no protocol, no trailing slash) * * @layer Domain */ /** * Domain Value Object * * Immutable representation of a validated domain name. */ export class Domain { private readonly value: string; private constructor(domain: string) { this.value = this.normalize(domain); this.validate(); } /** * Creates a new Domain instance * * @param domain - Domain name string * @returns Domain value object * @throws Error if domain is invalid */ static create(domain: string): Domain { return new Domain(domain); } /** * Normalizes domain name * * Removes: * - Protocol (http://, https://) * - www. prefix * - Trailing slashes * - Port numbers * * Converts to lowercase * * @param domain - Raw domain string * @returns Normalized domain */ private normalize(domain: string): string { let normalized = domain.toLowerCase().trim(); // Remove protocol normalized = normalized.replace(/^https?:\/\//, ''); // Remove www. prefix normalized = normalized.replace(/^www\./, ''); // Remove trailing slash normalized = normalized.replace(/\/$/, ''); // Remove port normalized = normalized.replace(/:\d+$/, ''); // Remove path normalized = normalized.split('/')[0]; return normalized; } /** * Validates domain format * * @throws Error if domain is invalid */ private validate(): void { if (!this.value || this.value.length === 0) { throw new Error('Domain cannot be empty'); } // Domain format validation (simplified) const domainRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/; if (!domainRegex.test(this.value)) { throw new Error(`Invalid domain format: ${this.value}`); } // Additional validations if (this.value.length > 253) { throw new Error('Domain too long (max 253 characters)'); } // Check for consecutive dots if (this.value.includes('..')) { throw new Error('Domain cannot contain consecutive dots'); } // Check for leading/trailing dots if (this.value.startsWith('.') || this.value.endsWith('.')) { throw new Error('Domain cannot start or end with a dot'); } } /** * Gets domain string * * @returns Domain name */ getValue(): string { return this.value; } /** * Gets top-level domain (TLD) * * @returns TLD (e.g., "com" from "example.com") */ getTLD(): string { const parts = this.value.split('.'); return parts[parts.length - 1]; } /** * Gets subdomain parts * * @returns Array of subdomain parts (e.g., ["mail", "example"] from "mail.example.com") */ getSubdomains(): string[] { const parts = this.value.split('.'); return parts.slice(0, -1); } /** * Gets root domain (without subdomains) * * @returns Root domain (e.g., "example.com" from "mail.example.com") */ getRootDomain(): string { const parts = this.value.split('.'); if (parts.length < 2) { return this.value; } return parts.slice(-2).join('.'); } /** * Checks if this is a subdomain of another domain * * @param other - Domain to check against * @returns true if this domain is a subdomain of other */ isSubdomainOf(other: Domain): boolean { return this.value.endsWith(`.${other.value}`) || this.value === other.value; } /** * Checks equality with another Domain * * @param other - Domain to compare * @returns true if domains are equal */ equals(other: Domain): boolean { return this.value === other.value; } /** * Converts to string * * @returns Domain string */ toString(): string { return this.value; } /** * Converts to JSON * * @returns Domain string */ toJSON(): string { return this.value; } /** * Creates a URL with protocol * * @param useHttps - Use HTTPS (default) or HTTP * @returns Full URL string */ toURL(useHttps = true): string { const protocol = useHttps ? 'https' : 'http'; return `${protocol}://${this.value}`; } }