/** * Port: MediaResolverPort * * Interface for resolving MediaReference objects to actual URLs. * Part of the clean architecture ports layer. * * Implementations: * - InMemoryMediaResolverAdapter (for tests/stubs) * - HttpMediaResolverAdapter (for production HTTP resolution) * - FileSystemMediaResolverAdapter (for local file resolution) */ import { MediaReference } from '@core/domain/media/MediaReference'; /** * MediaResolverPort interface * * Resolves a MediaReference to a concrete path string or null if no media exists. * Returns path-only URLs (e.g., /media/teams/123/logo) without baseUrl. * * @param ref - The media reference to resolve * @returns Promise resolving to a path string or null * * @example * ```typescript * const resolver: MediaResolverPort = new MediaResolverAdapter(); * const ref = MediaReference.createSystemDefault('avatar'); * const path = await resolver.resolve(ref); * // Returns: '/media/default/male-default-avatar.png' * ``` */ export interface MediaResolverPort { /** * Resolve a media reference to a path-only URL * * @param ref - The media reference to resolve * @returns Promise resolving to path string or null (for 'none' type or resolution failures) * * @throws Should not throw for valid inputs - returns null instead * @throws May throw for invalid inputs (null ref) */ resolve(ref: MediaReference): Promise; } /** * Type guard to check if an object implements MediaResolverPort */ export function isMediaResolverPort(obj: unknown): obj is MediaResolverPort { const typedObj = obj as MediaResolverPort; return ( obj !== null && typeof obj === 'object' && typeof typedObj.resolve === 'function' ); } /** * Default resolution strategies for different reference types * Returns path-only URLs */ export const ResolutionStrategies = { /** * Resolve system-default references * Format: /media/default/{variant} */ systemDefault: (ref: MediaReference): string | null => { if (ref.type !== 'system-default') return null; let filename: string; if (ref.variant === 'avatar' && ref.avatarVariant) { filename = `${ref.avatarVariant}-default-avatar.png`; } else if (ref.variant === 'avatar') { filename = `neutral-default-avatar.png`; } else { filename = `${ref.variant}.png`; } return `/media/default/${filename}`; }, /** * Resolve generated references * Format: /media/teams/{id}/logo, /media/leagues/{id}/logo, /media/avatar/{id} */ generated: (ref: MediaReference): string | null => { if (ref.type !== 'generated') return null; if (!ref.generationRequestId) { return null; } const firstHyphenIndex = ref.generationRequestId.indexOf('-'); if (firstHyphenIndex === -1) { return null; } const type = ref.generationRequestId.substring(0, firstHyphenIndex); const id = ref.generationRequestId.substring(firstHyphenIndex + 1); if (type === 'team') { return `/media/teams/${id}/logo`; } else if (type === 'league') { return `/media/leagues/${id}/logo`; } else if (type === 'driver') { return `/media/avatar/${id}`; } return `/media/generated/${type}/${id}`; }, /** * Resolve uploaded references * Format: /media/uploaded/{mediaId} */ uploaded: (ref: MediaReference): string | null => { if (ref.type !== 'uploaded') return null; if (!ref.mediaId) return null; return `/media/uploaded/${ref.mediaId}`; }, /** * Resolve none references * Returns: null */ // eslint-disable-next-line @typescript-eslint/no-unused-vars none: (_ref: MediaReference): string | null => { return null; }, } as const; /** * Helper function to resolve using default strategies */ export function resolveWithDefaults(ref: MediaReference): string | null { switch (ref.type) { case 'system-default': return ResolutionStrategies.systemDefault(ref); case 'generated': return ResolutionStrategies.generated(ref); case 'uploaded': return ResolutionStrategies.uploaded(ref); case 'none': return ResolutionStrategies.none(ref); default: // Exhaustive check - TypeScript will error if we miss a case return null; } }