harden media
This commit is contained in:
530
core/domain/media/MediaReference.test.ts
Normal file
530
core/domain/media/MediaReference.test.ts
Normal file
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* TDD Tests for MediaReference value object
|
||||
*
|
||||
* Tests cover:
|
||||
* - Discriminated union validation
|
||||
* - Type-specific validation
|
||||
* - Serialization/deserialization
|
||||
* - Equality comparison
|
||||
* - Hash generation
|
||||
*/
|
||||
|
||||
import { MediaReference } from '@core/domain/media/MediaReference';
|
||||
|
||||
describe('MediaReference', () => {
|
||||
describe('System Default Type', () => {
|
||||
it('should create system-default reference', () => {
|
||||
const ref = MediaReference.createSystemDefault();
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar'); // default
|
||||
});
|
||||
|
||||
it('should create system-default with custom variant', () => {
|
||||
const ref = MediaReference.createSystemDefault('logo');
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('logo');
|
||||
});
|
||||
|
||||
it('should create system-default with avatar variant', () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar', 'male');
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
expect(ref.avatarVariant).toBe('male');
|
||||
});
|
||||
|
||||
it('should create system-default without avatar variant for logo', () => {
|
||||
const ref = MediaReference.createSystemDefault('logo', 'male');
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('logo');
|
||||
expect(ref.avatarVariant).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should validate system-default type', () => {
|
||||
const ref = MediaReference.fromJSON({
|
||||
type: 'system-default',
|
||||
variant: 'avatar'
|
||||
});
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('should validate system-default with avatar variant', () => {
|
||||
const ref = MediaReference.fromJSON({
|
||||
type: 'system-default',
|
||||
variant: 'avatar',
|
||||
avatarVariant: 'female'
|
||||
});
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
expect(ref.avatarVariant).toBe('female');
|
||||
});
|
||||
|
||||
it('should reject system-default with invalid variant', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'system-default',
|
||||
variant: 'invalid' as any
|
||||
});
|
||||
}).toThrow('Invalid variant');
|
||||
});
|
||||
|
||||
it('should reject system-default with invalid avatar variant', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'system-default',
|
||||
variant: 'avatar',
|
||||
avatarVariant: 'invalid' as any
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('New Static Methods', () => {
|
||||
it('should create system-default using systemDefault method', () => {
|
||||
const ref = MediaReference.systemDefault('avatar');
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
});
|
||||
|
||||
it('should create system-default with avatar variant using systemDefault method', () => {
|
||||
const ref = MediaReference.systemDefault('male');
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
expect(ref.avatarVariant).toBe('male');
|
||||
});
|
||||
|
||||
it('should create generated using generated method', () => {
|
||||
const ref = MediaReference.generated('team', 'team-123');
|
||||
|
||||
expect(ref.type).toBe('generated');
|
||||
expect(ref.generationRequestId).toBe('team-team-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Generated Type', () => {
|
||||
it('should create generated reference with request ID', () => {
|
||||
const ref = MediaReference.createGenerated('req-123');
|
||||
|
||||
expect(ref.type).toBe('generated');
|
||||
expect(ref.generationRequestId).toBe('req-123');
|
||||
});
|
||||
|
||||
it('should validate generated type', () => {
|
||||
const ref = MediaReference.fromJSON({
|
||||
type: 'generated',
|
||||
generationRequestId: 'req-456'
|
||||
});
|
||||
|
||||
expect(ref.type).toBe('generated');
|
||||
expect(ref.generationRequestId).toBe('req-456');
|
||||
});
|
||||
|
||||
it('should reject generated without request ID', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'generated'
|
||||
} as any);
|
||||
}).toThrow('Generation request ID is required');
|
||||
});
|
||||
|
||||
it('should reject generated with empty request ID', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'generated',
|
||||
generationRequestId: ''
|
||||
});
|
||||
}).toThrow('Generation request ID is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uploaded Type', () => {
|
||||
it('should create uploaded reference with media ID', () => {
|
||||
const ref = MediaReference.createUploaded('media-789');
|
||||
|
||||
expect(ref.type).toBe('uploaded');
|
||||
expect(ref.mediaId).toBe('media-789');
|
||||
});
|
||||
|
||||
it('should validate uploaded type', () => {
|
||||
const ref = MediaReference.fromJSON({
|
||||
type: 'uploaded',
|
||||
mediaId: 'media-abc'
|
||||
});
|
||||
|
||||
expect(ref.type).toBe('uploaded');
|
||||
expect(ref.mediaId).toBe('media-abc');
|
||||
});
|
||||
|
||||
it('should reject uploaded without media ID', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'uploaded'
|
||||
} as any);
|
||||
}).toThrow('Media ID is required');
|
||||
});
|
||||
|
||||
it('should reject uploaded with empty media ID', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'uploaded',
|
||||
mediaId: ''
|
||||
});
|
||||
}).toThrow('Media ID is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('None Type', () => {
|
||||
it('should create none reference', () => {
|
||||
const ref = MediaReference.createNone();
|
||||
|
||||
expect(ref.type).toBe('none');
|
||||
});
|
||||
|
||||
it('should validate none type', () => {
|
||||
const ref = MediaReference.fromJSON({
|
||||
type: 'none'
|
||||
});
|
||||
|
||||
expect(ref.type).toBe('none');
|
||||
});
|
||||
|
||||
it('should reject none with extra properties', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'none',
|
||||
mediaId: 'should-not-exist'
|
||||
} as any);
|
||||
}).toThrow('None type should not have additional properties');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Invalid Types', () => {
|
||||
it('should reject unknown type', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({
|
||||
type: 'unknown'
|
||||
} as any);
|
||||
}).toThrow('Invalid type');
|
||||
});
|
||||
|
||||
it('should reject missing type', () => {
|
||||
expect(() => {
|
||||
MediaReference.fromJSON({} as any);
|
||||
}).toThrow('Type is required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Serialization', () => {
|
||||
it('should serialize system-default to JSON', () => {
|
||||
const ref = MediaReference.createSystemDefault('logo');
|
||||
const json = ref.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'system-default',
|
||||
variant: 'logo'
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize system-default with avatar variant to JSON', () => {
|
||||
const ref = MediaReference.createSystemDefault('avatar', 'female');
|
||||
const json = ref.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'system-default',
|
||||
variant: 'avatar',
|
||||
avatarVariant: 'female'
|
||||
});
|
||||
});
|
||||
|
||||
it('should deserialize system-default with avatar variant from JSON', () => {
|
||||
const json = {
|
||||
type: 'system-default',
|
||||
variant: 'avatar',
|
||||
avatarVariant: 'neutral'
|
||||
};
|
||||
const ref = MediaReference.fromJSON(json as unknown as Record<string, unknown>);
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.variant).toBe('avatar');
|
||||
expect(ref.avatarVariant).toBe('neutral');
|
||||
});
|
||||
|
||||
it('should serialize generated to JSON', () => {
|
||||
const ref = MediaReference.createGenerated('req-123');
|
||||
const json = ref.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'generated',
|
||||
generationRequestId: 'req-123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize uploaded to JSON', () => {
|
||||
const ref = MediaReference.createUploaded('media-456');
|
||||
const json = ref.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'uploaded',
|
||||
mediaId: 'media-456'
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize none to JSON', () => {
|
||||
const ref = MediaReference.createNone();
|
||||
const json = ref.toJSON();
|
||||
|
||||
expect(json).toEqual({
|
||||
type: 'none'
|
||||
});
|
||||
});
|
||||
|
||||
it('should deserialize from JSON', () => {
|
||||
const json = {
|
||||
type: 'uploaded',
|
||||
mediaId: 'media-789'
|
||||
};
|
||||
const ref = MediaReference.fromJSON(json);
|
||||
|
||||
expect(ref.type).toBe('uploaded');
|
||||
expect(ref.mediaId).toBe('media-789');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Equality', () => {
|
||||
it('should be equal for same system-default with same variant', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be equal for system-default with different variants', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar');
|
||||
const ref2 = MediaReference.createSystemDefault('logo');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be equal for system-default with different avatar variants', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar', 'female');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be equal for system-default with same avatar variant', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be equal for system-default with and without avatar variant', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be equal for same generated with same request ID', () => {
|
||||
const ref1 = MediaReference.createGenerated('req-123');
|
||||
const ref2 = MediaReference.createGenerated('req-123');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be equal for generated with different request IDs', () => {
|
||||
const ref1 = MediaReference.createGenerated('req-123');
|
||||
const ref2 = MediaReference.createGenerated('req-456');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be equal for same uploaded with same media ID', () => {
|
||||
const ref1 = MediaReference.createUploaded('media-123');
|
||||
const ref2 = MediaReference.createUploaded('media-123');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be equal for uploaded with different media IDs', () => {
|
||||
const ref1 = MediaReference.createUploaded('media-123');
|
||||
const ref2 = MediaReference.createUploaded('media-456');
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be equal for none references', () => {
|
||||
const ref1 = MediaReference.createNone();
|
||||
const ref2 = MediaReference.createNone();
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be equal for different types', () => {
|
||||
const ref1 = MediaReference.createSystemDefault();
|
||||
const ref2 = MediaReference.createNone();
|
||||
|
||||
expect(ref1.equals(ref2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not be equal to non-MediaReference', () => {
|
||||
const ref = MediaReference.createSystemDefault();
|
||||
|
||||
expect(ref.equals({} as any)).toBe(false);
|
||||
expect(ref.equals(null as any)).toBe(false);
|
||||
expect(ref.equals(undefined as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Hash', () => {
|
||||
it('should generate consistent hash for same system-default', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
expect(ref1.hash()).toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for different variants', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar');
|
||||
const ref2 = MediaReference.createSystemDefault('logo');
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for different avatar variants', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar', 'female');
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate same hash for same avatar variant', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
|
||||
expect(ref1.hash()).toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for system-default with and without avatar variant', () => {
|
||||
const ref1 = MediaReference.createSystemDefault('avatar', 'male');
|
||||
const ref2 = MediaReference.createSystemDefault('avatar');
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate consistent hash for same generated', () => {
|
||||
const ref1 = MediaReference.createGenerated('req-123');
|
||||
const ref2 = MediaReference.createGenerated('req-123');
|
||||
|
||||
expect(ref1.hash()).toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for different request IDs', () => {
|
||||
const ref1 = MediaReference.createGenerated('req-123');
|
||||
const ref2 = MediaReference.createGenerated('req-456');
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate consistent hash for same uploaded', () => {
|
||||
const ref1 = MediaReference.createUploaded('media-123');
|
||||
const ref2 = MediaReference.createUploaded('media-123');
|
||||
|
||||
expect(ref1.hash()).toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for different media IDs', () => {
|
||||
const ref1 = MediaReference.createUploaded('media-123');
|
||||
const ref2 = MediaReference.createUploaded('media-456');
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate same hash for none references', () => {
|
||||
const ref1 = MediaReference.createNone();
|
||||
const ref2 = MediaReference.createNone();
|
||||
|
||||
expect(ref1.hash()).toBe(ref2.hash());
|
||||
});
|
||||
|
||||
it('should generate different hash for different types', () => {
|
||||
const ref1 = MediaReference.createSystemDefault();
|
||||
const ref2 = MediaReference.createNone();
|
||||
|
||||
expect(ref1.hash()).not.toBe(ref2.hash());
|
||||
});
|
||||
});
|
||||
|
||||
describe('Type Guards', () => {
|
||||
it('should correctly identify system-default type', () => {
|
||||
const ref = MediaReference.createSystemDefault();
|
||||
|
||||
expect(ref.type).toBe('system-default');
|
||||
expect(ref.generationRequestId).toBeUndefined();
|
||||
expect(ref.mediaId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly identify generated type', () => {
|
||||
const ref = MediaReference.createGenerated('req-123');
|
||||
|
||||
expect(ref.type).toBe('generated');
|
||||
expect(ref.generationRequestId).toBe('req-123');
|
||||
expect(ref.mediaId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly identify uploaded type', () => {
|
||||
const ref = MediaReference.createUploaded('media-123');
|
||||
|
||||
expect(ref.type).toBe('uploaded');
|
||||
expect(ref.mediaId).toBe('media-123');
|
||||
expect(ref.generationRequestId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should correctly identify none type', () => {
|
||||
const ref = MediaReference.createNone();
|
||||
|
||||
expect(ref.type).toBe('none');
|
||||
expect(ref.generationRequestId).toBeUndefined();
|
||||
expect(ref.mediaId).toBeUndefined();
|
||||
expect(ref.variant).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle whitespace in IDs', () => {
|
||||
const ref = MediaReference.createGenerated(' req-123 ');
|
||||
|
||||
expect(ref.generationRequestId).toBe('req-123');
|
||||
});
|
||||
|
||||
it('should handle special characters in IDs', () => {
|
||||
const ref = MediaReference.createUploaded('media-abc-123_XYZ');
|
||||
|
||||
expect(ref.mediaId).toBe('media-abc-123_XYZ');
|
||||
});
|
||||
|
||||
it('should preserve exact string values', () => {
|
||||
const id = 'CaseSensitive-ID_123';
|
||||
const ref = MediaReference.createUploaded(id);
|
||||
|
||||
expect(ref.mediaId).toBe(id);
|
||||
});
|
||||
|
||||
it('should handle JSON round-trip', () => {
|
||||
const original = MediaReference.createGenerated('req-999');
|
||||
const json = original.toJSON();
|
||||
const restored = MediaReference.fromJSON(json as unknown as Record<string, unknown>);
|
||||
|
||||
expect(restored.equals(original)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
286
core/domain/media/MediaReference.ts
Normal file
286
core/domain/media/MediaReference.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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 { IValueObject } from '@core/shared/domain';
|
||||
|
||||
// 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 IValueObject<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: IValueObject<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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user