/** * Domain Value Object: MediaReference * * Discriminated union representing different types of media references: * - system-default: Pre-defined default media (e.g., default avatar/logo) * - generated: Media generated by AI/algorithm (references generation request) * - uploaded: User-uploaded media (references media entity) * - none: Explicit no-media placeholder * * Follows clean architecture and TDD principles. */ import type { ValueObject } from '@core/shared/domain/ValueObject'; // Variant types for system-default references export type MediaVariant = 'avatar' | 'logo'; // Specific variants for avatars (deterministic selection) export type AvatarVariant = 'male' | 'female' | 'neutral'; // Discriminated union types export interface SystemDefaultRef { type: 'system-default'; variant: MediaVariant; avatarVariant?: AvatarVariant | undefined; // Only used when variant is 'avatar' } export interface GeneratedRef { type: 'generated'; generationRequestId: string; } export interface UploadedRef { type: 'uploaded'; mediaId: string; } export interface NoneRef { type: 'none'; } // Union of all reference types export type MediaReferenceProps = SystemDefaultRef | GeneratedRef | UploadedRef | NoneRef; // Type guards function isSystemDefaultRef(props: unknown): props is SystemDefaultRef { const typedProps = props as SystemDefaultRef; if (typedProps?.type !== 'system-default') { return false; } if (typedProps.variant !== 'avatar' && typedProps.variant !== 'logo') { return false; } // avatarVariant is optional, but if present must be valid if (typedProps.avatarVariant !== undefined) { return typedProps.avatarVariant === 'male' || typedProps.avatarVariant === 'female' || typedProps.avatarVariant === 'neutral'; } return true; } function isGeneratedRef(props: unknown): props is GeneratedRef { const typedProps = props as GeneratedRef; return typedProps?.type === 'generated' && typeof typedProps.generationRequestId === 'string' && typedProps.generationRequestId.trim().length > 0; } function isUploadedRef(props: unknown): props is UploadedRef { const typedProps = props as UploadedRef; return typedProps?.type === 'uploaded' && typeof typedProps.mediaId === 'string' && typedProps.mediaId.trim().length > 0; } function isNoneRef(props: unknown): props is NoneRef { const typedProps = props as NoneRef; return typedProps?.type === 'none' && Object.keys(typedProps).length === 1; // Only 'type' property } export class MediaReference implements ValueObject { public readonly props: MediaReferenceProps; private constructor(props: MediaReferenceProps) { this.props = props; } /** * Factory method to create system-default reference */ static createSystemDefault(variant: MediaVariant = 'avatar', avatarVariant?: AvatarVariant): MediaReference { const props: SystemDefaultRef = { type: 'system-default', variant }; if (variant === 'avatar' && avatarVariant) { props.avatarVariant = avatarVariant; } return new MediaReference(props); } /** * Factory method for system default (alias for task compatibility) */ static systemDefault(variant: MediaVariant | AvatarVariant = 'avatar'): MediaReference { // If it's an avatar variant, use it if (variant === 'male' || variant === 'female' || variant === 'neutral') { return new MediaReference({ type: 'system-default', variant: 'avatar', avatarVariant: variant }); } // Otherwise it's a regular variant return new MediaReference({ type: 'system-default', variant: variant as MediaVariant }); } /** * Factory method for generated references (alias for task compatibility) */ static generated(type: string, id: string): MediaReference { return new MediaReference({ type: 'generated', generationRequestId: `${type}-${id}` }); } /** * Factory method to create generated reference */ static createGenerated(generationRequestId: string): MediaReference { const trimmed = generationRequestId.trim(); if (!trimmed) { throw new Error('Generation request ID is required'); } return new MediaReference({ type: 'generated', generationRequestId: trimmed }); } /** * Factory method to create uploaded reference */ static createUploaded(mediaId: string): MediaReference { const trimmed = mediaId.trim(); if (!trimmed) { throw new Error('Media ID is required'); } return new MediaReference({ type: 'uploaded', mediaId: trimmed }); } /** * Factory method to create none reference */ static createNone(): MediaReference { return new MediaReference({ type: 'none' }); } /** * Deserialize from JSON */ static fromJSON(json: Record): MediaReference { if (!json || typeof json !== 'object') { throw new Error('Invalid JSON: must be an object'); } const type = json.type; if (!type || typeof type !== 'string') { throw new Error('Type is required'); } switch (type) { case 'system-default': if (!isSystemDefaultRef(json)) { throw new Error('Invalid variant for system-default. Must be "avatar" or "logo"'); } return new MediaReference({ type: 'system-default', variant: json.variant, avatarVariant: json.avatarVariant }); case 'generated': if (!isGeneratedRef(json)) { throw new Error('Generation request ID is required'); } return new MediaReference({ type: 'generated', generationRequestId: json.generationRequestId }); case 'uploaded': if (!isUploadedRef(json)) { throw new Error('Media ID is required'); } return new MediaReference({ type: 'uploaded', mediaId: json.mediaId }); case 'none': if (!isNoneRef(json)) { throw new Error('None type should not have additional properties'); } return new MediaReference({ type: 'none' }); default: throw new Error('Invalid type'); } } /** * Serialize to JSON */ toJSON(): MediaReferenceProps { return { ...this.props }; } /** * Get the type of this reference */ get type(): MediaReferenceProps['type'] { return this.props.type; } /** * Get variant (only for system-default) */ get variant(): MediaVariant | undefined { return this.props.type === 'system-default' ? this.props.variant : undefined; } /** * Get avatar variant (only for system-default with avatar variant) */ get avatarVariant(): AvatarVariant | undefined { return this.props.type === 'system-default' ? this.props.avatarVariant : undefined; } /** * Get generation request ID (only for generated) */ get generationRequestId(): string | undefined { return this.props.type === 'generated' ? this.props.generationRequestId : undefined; } /** * Get media ID (only for uploaded) */ get mediaId(): string | undefined { return this.props.type === 'uploaded' ? this.props.mediaId : undefined; } /** * Equality comparison */ equals(other: ValueObject): boolean { if (!(other instanceof MediaReference)) { return false; } const a = this.props; const b = other.props; if (a.type !== b.type) { return false; } switch (a.type) { case 'system-default': return b.type === 'system-default' && a.variant === b.variant && a.avatarVariant === b.avatarVariant; case 'generated': return b.type === 'generated' && a.generationRequestId === b.generationRequestId; case 'uploaded': return b.type === 'uploaded' && a.mediaId === b.mediaId; case 'none': return b.type === 'none'; default: return false; } } /** * Generate hash for this reference * Used for caching and comparison */ hash(): string { switch (this.props.type) { case 'system-default': return `system-default:${this.props.variant}${this.props.avatarVariant ? `:${this.props.avatarVariant}` : ''}`; case 'generated': return `generated:${this.props.generationRequestId}`; case 'uploaded': return `uploaded:${this.props.mediaId}`; case 'none': return 'none'; default: // Exhaustive check - should never reach here throw new Error('Unknown type'); } } }