parent
e73fcc1e73
commit
97455f5437
@ -0,0 +1,96 @@ |
|||||||
|
import { describe, it, expect } from '@jest/globals'; |
||||||
|
import movie3532674 from '../../../resources/movies/3532674.json'; |
||||||
|
import movie5979300 from '../../../resources/movies/5979300.json'; |
||||||
|
import movie11043689 from '../../../resources/movies/11043689.json'; |
||||||
|
import movie11528860 from '../../../resources/movies/11528860.json'; |
||||||
|
import { normalizeRawInternalData } from './converters'; |
||||||
|
|
||||||
|
describe('normalizeRawInternalData', () => { |
||||||
|
it('normalizes data correctly for 3532674', () => { |
||||||
|
const result = normalizeRawInternalData(movie3532674); |
||||||
|
expect(result).toEqual({ |
||||||
|
availableLanguages: ['de', 'en'], |
||||||
|
duration: 119, |
||||||
|
imdbId: 'tt0401792', |
||||||
|
internalId: 3532674, |
||||||
|
localDescription: |
||||||
|
'Im Sündenpfuhl Sin City werden drei Geschichten parallel erzählt: Der Polizist Hartigan jagt einen Pädophilen, der Outlaw Dwight muss im Rotlichtbezirk untertauchen und dem Schläger Marv wird ein Mord angehängt. ', |
||||||
|
localTitle: 'Sin City', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 2005, |
||||||
|
ratings: [ |
||||||
|
{ |
||||||
|
source: 'Internal', |
||||||
|
value: '3.9/5', |
||||||
|
}, |
||||||
|
], |
||||||
|
studios: ['Studiocanal', 'Paramount'], |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('normalizes data correctly for 5979300', () => { |
||||||
|
const result = normalizeRawInternalData(movie5979300); |
||||||
|
expect(result).toEqual({ |
||||||
|
availableLanguages: ['de', 'en'], |
||||||
|
duration: 127, |
||||||
|
imdbId: 'tt0097576', |
||||||
|
internalId: 5979300, |
||||||
|
localDescription: |
||||||
|
'1912: Der junge Indiana Jones will Grabräubern das Kreuz von Coronado abluchsen, um es in ein Museum zu bringen. Nach einem Zeitsprung ins Jahr 1938 kämpft Indy wieder um das Kreuz, diesmal erfolgreich. Anschließend soll er den Heiligen Gral suchen.', |
||||||
|
localTitle: 'Indiana Jones und der letzte Kreuzzug', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 1989, |
||||||
|
ratings: [ |
||||||
|
{ |
||||||
|
source: 'Internal', |
||||||
|
value: '4.2/5', |
||||||
|
}, |
||||||
|
], |
||||||
|
studios: ['Paramount'], |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('normalizes data correctly for 11043689', () => { |
||||||
|
const result = normalizeRawInternalData(movie11043689); |
||||||
|
expect(result).toEqual({ |
||||||
|
availableLanguages: ['de', 'en'], |
||||||
|
duration: 120, |
||||||
|
imdbId: 'tt0076759', |
||||||
|
internalId: 11043689, |
||||||
|
localDescription: |
||||||
|
'Der im Exil lebende Jedi-Ritter Obi-Wan Kenobi nimmt sich einen Mann namens Luke Skywalker als Schüler. Zusammen helfen sie der Rebellion, die Pläne des bösen Imperiums und des Sith-Lords Darth Vader zu vereiteln. ', |
||||||
|
localTitle: 'Star Wars: Eine neue Hoffnung', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 1977, |
||||||
|
ratings: [ |
||||||
|
{ |
||||||
|
source: 'Internal', |
||||||
|
value: '4.5/5', |
||||||
|
}, |
||||||
|
], |
||||||
|
studios: ['FOX', 'Paramount'], |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('normalizes data correctly for 11528860', () => { |
||||||
|
const result = normalizeRawInternalData(movie11528860); |
||||||
|
expect(result).toEqual({ |
||||||
|
availableLanguages: ['de', 'en'], |
||||||
|
duration: 75, |
||||||
|
imdbId: 'tt0061852', |
||||||
|
internalId: 11528860, |
||||||
|
localDescription: |
||||||
|
'Unter der Obhut des Panthers Baghira wächst das Findelkind Mogli bei einer Wolfsfamilie auf. Doch da erschüttert die Rückkehr des menschenfressenden Tigers Shir Khan den Dschungel. Die Sorge um Mogli zwingt Baghira zu der einzig möglichen Entscheidung.', |
||||||
|
localTitle: 'Das Dschungelbuch', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 1967, |
||||||
|
ratings: [ |
||||||
|
{ |
||||||
|
source: 'Internal', |
||||||
|
value: '4.4/5', |
||||||
|
}, |
||||||
|
], |
||||||
|
studios: ['Disney'], |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,54 @@ |
|||||||
|
import { createTargetHelpers, identity } from '../../../utils/objectHelpers'; |
||||||
|
import { NormalizedInternalData } from '../types'; |
||||||
|
import { StoredData } from './types'; |
||||||
|
|
||||||
|
const { createRequiredField } = createTargetHelpers<NormalizedInternalData>([ |
||||||
|
null, |
||||||
|
'', |
||||||
|
]); |
||||||
|
|
||||||
|
const internalRatingToRatings = (internalRating: StoredData['userrating']) => { |
||||||
|
let totalStars = 0; |
||||||
|
for (let i = 1; i <= 5; i++) { |
||||||
|
totalStars += internalRating[`countStar${i as 1 | 2 | 3 | 4 | 5}`] * i; |
||||||
|
} |
||||||
|
|
||||||
|
const rating = totalStars / internalRating.countTotal; |
||||||
|
|
||||||
|
return [ |
||||||
|
{ |
||||||
|
source: 'Internal', |
||||||
|
value: `${rating.toFixed(1)}/5`, |
||||||
|
}, |
||||||
|
]; |
||||||
|
}; |
||||||
|
|
||||||
|
export const normalizeRawInternalData = ( |
||||||
|
rawInternalData: StoredData, |
||||||
|
): NormalizedInternalData => { |
||||||
|
const { |
||||||
|
description, |
||||||
|
duration, |
||||||
|
id, |
||||||
|
imdbId, |
||||||
|
languages, |
||||||
|
originalLanguage, |
||||||
|
productionYear, |
||||||
|
studios, |
||||||
|
title, |
||||||
|
userrating, |
||||||
|
} = rawInternalData; |
||||||
|
|
||||||
|
return { |
||||||
|
...createRequiredField('localDescription', description, identity), |
||||||
|
...createRequiredField('duration', duration, identity), |
||||||
|
...createRequiredField('internalId', id, identity<number>), |
||||||
|
...createRequiredField('imdbId', imdbId, identity), |
||||||
|
...createRequiredField('availableLanguages', languages, identity), |
||||||
|
...createRequiredField('originalLanguage', originalLanguage, identity), |
||||||
|
...createRequiredField('productionYear', productionYear, identity), |
||||||
|
...createRequiredField('studios', studios, identity), |
||||||
|
...createRequiredField('localTitle', title, identity), |
||||||
|
...createRequiredField('ratings', userrating, internalRatingToRatings), |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,59 @@ |
|||||||
|
import { describe, it, expect } from '@jest/globals'; |
||||||
|
|
||||||
|
import { createInternalProvider } from './index'; |
||||||
|
|
||||||
|
describe('createInternalProvider', () => { |
||||||
|
const client = createInternalProvider(); |
||||||
|
|
||||||
|
it('returns correct data for 11528860', async () => { |
||||||
|
const result = await client.getMetadataByInternalId(11528860); |
||||||
|
expect(result).toMatchObject({ |
||||||
|
availableLanguages: expect.any(Array), |
||||||
|
duration: 75, |
||||||
|
internalId: 11528860, |
||||||
|
imdbId: 'tt0061852', |
||||||
|
localDescription: expect.any(String), |
||||||
|
localTitle: 'Das Dschungelbuch', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 1967, |
||||||
|
studios: ['Disney'], |
||||||
|
ratings: expect.arrayContaining([ |
||||||
|
{ |
||||||
|
source: expect.any(String), |
||||||
|
value: expect.any(String), |
||||||
|
}, |
||||||
|
]), |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns undefined for non-existent internal id', async () => { |
||||||
|
const result = await client.getMetadataByInternalId(999999); |
||||||
|
expect(result).toBeUndefined(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns data for imdb id', async () => { |
||||||
|
const result = await client.getMetadataByImdbId('tt0061852'); |
||||||
|
expect(result).toMatchObject({ |
||||||
|
availableLanguages: expect.any(Array), |
||||||
|
duration: 75, |
||||||
|
internalId: 11528860, |
||||||
|
imdbId: 'tt0061852', |
||||||
|
localDescription: expect.any(String), |
||||||
|
localTitle: 'Das Dschungelbuch', |
||||||
|
originalLanguage: 'en', |
||||||
|
productionYear: 1967, |
||||||
|
studios: ['Disney'], |
||||||
|
ratings: expect.arrayContaining([ |
||||||
|
{ |
||||||
|
source: expect.any(String), |
||||||
|
value: expect.any(String), |
||||||
|
}, |
||||||
|
]), |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns undefined for non-existent imdb id', async () => { |
||||||
|
const result = await client.getMetadataByImdbId('tt99999999999'); |
||||||
|
expect(result).toBeUndefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,28 @@ |
|||||||
|
import { InternalProvider } from '../types'; |
||||||
|
import { normalizeRawInternalData } from './converters'; |
||||||
|
import { createStorageClient } from './storage'; |
||||||
|
|
||||||
|
export const createInternalProvider = (): InternalProvider => { |
||||||
|
const storageClient = createStorageClient(); |
||||||
|
|
||||||
|
return { |
||||||
|
getMetadataByInternalId: async (internalId: number) => { |
||||||
|
const rawInternalData = |
||||||
|
await storageClient.getMetadataByInternalId(internalId); |
||||||
|
if (!rawInternalData) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
return normalizeRawInternalData(rawInternalData); |
||||||
|
}, |
||||||
|
getMetadataByImdbId: async (imdbId: string) => { |
||||||
|
const rawInternalData = |
||||||
|
await storageClient.getMetadataByImdbId(imdbId); |
||||||
|
if (!rawInternalData) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
return normalizeRawInternalData(rawInternalData); |
||||||
|
}, |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,50 @@ |
|||||||
|
export const identity = <T>(value: T) => value; |
||||||
|
|
||||||
|
export const createTargetHelpers = <TTarget>( |
||||||
|
missingValueIndicators: unknown[], |
||||||
|
) => { |
||||||
|
const createRequiredField = < |
||||||
|
TKey extends string & keyof TTarget, |
||||||
|
TRawValue, |
||||||
|
>( |
||||||
|
key: TKey, |
||||||
|
value: TRawValue, |
||||||
|
transformer: (value: TRawValue) => TTarget[TKey], |
||||||
|
) => { |
||||||
|
if (value === undefined) { |
||||||
|
throw new Error(`Value for ${key} is not defined`); |
||||||
|
} |
||||||
|
|
||||||
|
if (missingValueIndicators.includes(value)) { |
||||||
|
throw new Error(`Missing value for ${key}`); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
[key]: transformer(value), |
||||||
|
} as Record<TKey, TTarget[TKey]>; |
||||||
|
}; |
||||||
|
|
||||||
|
const createOptionalField = < |
||||||
|
TKey extends string & keyof TTarget, |
||||||
|
TRawValue, |
||||||
|
>( |
||||||
|
key: TKey, |
||||||
|
value: TRawValue, |
||||||
|
transformer: (value: Exclude<TRawValue, undefined>) => TTarget[TKey], |
||||||
|
) => { |
||||||
|
if (value === undefined || missingValueIndicators.includes(value)) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
|
||||||
|
return createRequiredField( |
||||||
|
key, |
||||||
|
value as Exclude<TRawValue, undefined>, |
||||||
|
transformer, |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
return { |
||||||
|
createRequiredField, |
||||||
|
createOptionalField, |
||||||
|
}; |
||||||
|
}; |
Loading…
Reference in new issue