Files
gridpilot.gg/core/domain/media/MediaReference.ts
2026-01-16 16:46:57 +01:00

286 lines
8.3 KiB
TypeScript

/**
* 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<MediaReferenceProps> {
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<string, unknown>): 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<MediaReferenceProps>): 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');
}
}
}