diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts index f6b43606..423d0ed6 100644 --- a/src/backend/common/infrastructure/config/client/clients.ts +++ b/src/backend/common/infrastructure/config/client/clients.ts @@ -1,12 +1,14 @@ import * as z from "zod"; -import {koitoClientAIOConfigSchema, koitoClientConfigSchema} from "./koito.ts"; -import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema} from "./lastfm.ts"; -import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema} from "./listenbrainz.ts"; -import {malojaClientAIOConfigSchema, malojaClientConfigSchema} from "./maloja.ts"; -import {tealClientAIOConfigSchema, tealClientConfigSchema} from "./tealfm.ts"; -import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema} from "./rocksky.ts"; -import {librefmClientAIOConfigSchema, librefmClientConfigSchema} from "./librefm.ts"; -import {discordClientAIOConfigSchema, discordClientConfigSchema} from "./discord.ts"; +import {koitoClientAIOConfigSchema, koitoClientConfigSchema, type KoitoClientConfig} from "./koito.ts"; +import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema, type LastfmClientConfig} from "./lastfm.ts"; +import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema, type ListenBrainzClientConfig} from "./listenbrainz.ts"; +import {malojaClientAIOConfigSchema, malojaClientConfigSchema, type MalojaClientConfig} from "./maloja.ts"; +import {tealClientAIOConfigSchema, tealClientConfigSchema, type TealClientConfig} from "./tealfm.ts"; +import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema, type RockSkyClientConfig} from "./rocksky.ts"; +import {librefmClientAIOConfigSchema, librefmClientConfigSchema, type LibrefmClientConfig} from "./librefm.ts"; +import {discordClientAIOConfigSchema, discordClientConfigSchema, type DiscordClientConfig} from "./discord.ts"; +import type { CommonClientConfig } from "./index.ts"; +import type { ClientType } from "../../../../../core/Atomic.ts"; export const clientConfigSchema = z.union([ malojaClientConfigSchema, @@ -87,3 +89,27 @@ export const clientInterfaces = [ 'AIOClientRelaxedConfig', ...atomicClientInterfaces ]; + +export interface ClientTypeConfigMap extends Record { + maloja: MalojaClientConfig, + lastfm: LastfmClientConfig, + librefm: LibrefmClientConfig, + listenbrainz: ListenBrainzClientConfig, + koito: KoitoClientConfig, + tealfm: TealClientConfig, + rocksky: RockSkyClientConfig, + discord: DiscordClientConfig +} + +const clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: z.ZodType } = { + maloja: malojaClientConfigSchema, + lastfm: lastfmClientConfigSchema, + librefm: librefmClientConfigSchema, + listenbrainz: listenBrainzClientConfigSchema, + koito: koitoClientConfigSchema, + tealfm: tealClientConfigSchema, + rocksky: rockSkyClientConfigSchema, + discord: discordClientConfigSchema +} + +export const validateClientJson = (clientType: T, json: object): ClientTypeConfigMap[T] => clientConfigSchemaMap[clientType].parse(json); \ No newline at end of file diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 4b657c84..6dc68262 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -7,7 +7,7 @@ import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; import type {AIOConfig} from "../common/infrastructure/config/aioConfig.ts"; -import type {ClientAIOConfig, ClientConfig} from "../common/infrastructure/config/client/clients.ts"; +import {validateClientJson, type ClientAIOConfig, type ClientConfig} from "../common/infrastructure/config/client/clients.ts"; import type {LastfmClientConfig, LastfmData} from "../common/infrastructure/config/client/lastfm.ts"; import type {ListenBrainzClientConfig, ListenBrainzData} from "../common/infrastructure/config/client/listenbrainz.ts"; import type {MalojaClientConfig, MalojaData} from "../common/infrastructure/config/client/maloja.ts"; @@ -28,6 +28,7 @@ import clone from 'clone'; import type {DiscordClientConfig, DiscordData} from '../common/infrastructure/config/client/discord.ts'; import { stripIndents } from 'common-tags'; import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUtils.ts'; +import { prettifyError, ZodError } from 'zod'; type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; @@ -383,7 +384,7 @@ export default class ScrobbleClients { continue; } try { - const validConfig = await validateJson('client', rawConf, this.getSchemaByType(clientType), this.logger); + const validConfig = await validateClientJson(clientType, rawConf); // await validateJson('client', rawConf, this.getSchemaByType(clientType), this.logger); const {configureAs = defaultConfigureAs} = validConfig; if (configureAs === 'client') { const parsedConfig: ParsedConfig = { @@ -394,9 +395,15 @@ export default class ScrobbleClients { configs.push(parsedConfig); } } catch (e: any) { - const configErr = new Error(`The config entry at index ${i} from ${clientType}.json was not valid`, {cause: e}); + const msg = `The config entry at index ${i} from ${clientType}.json was not valid`; + const configErr = new Error(msg, {cause: e}); this.emitter.emit('error', configErr); - this.logger.error(configErr); + // pretty print error if its a zod error + if(e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(configErr); + } } } } diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index 52e03194..61ef5efc 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -16,6 +16,7 @@ import { difference } from '../../utils.ts'; import { validateSourceJson } from '../../common/infrastructure/config/source/sources.ts'; import { readJson } from '../../utils/DataUtils.ts'; import { prettifyError, ZodError } from 'zod'; +import { validateClientJson } from '../../common/infrastructure/config/client/clients.ts'; chai.use(asPromised); @@ -111,7 +112,29 @@ describe('Sample Configs', function () { }); for(const componentType of clientTypes) { - it(`Sample ${componentType}.json parses and validates`, async function () { + + it(`Sample ${componentType}.json parses and validates in isolation`, async function () { + this.timeout(5000); + + const emitter = new EventEmitter(); + await copyFile(samplePath(componentType), `${componentType}.json`); + + let fileContents = await readJson(`${componentType}.json`); + fileContents = fileContents.filter(x => x.configureAs === undefined || x.configureAs === 'client'); + for (const [i,rawConf] of fileContents.entries()) { + try { + validateClientJson(componentType, rawConf); + } catch (e) { + if(e instanceof ZodError) { + expect.fail(`Validation failed for config entry ${i}:\n${prettifyError(e)}`); + } else { + throw e; + } + } + } + }); + + it(`Sample ${componentType}.json parses and validates in ScrobbleClients`, async function () { this.timeout(5000); const emitter = new EventEmitter();