From 8345779030f4a3b0752525bd1d4e079085875e87 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Tue, 28 Jul 2026 16:43:51 +0000 Subject: [PATCH] refactor: cleaned up aio validation and client/source map --- .../infrastructure/config/client/clients.ts | 72 ++++++++----------- .../infrastructure/config/source/sources.ts | 15 +++- src/backend/scrobblers/ScrobbleClients.ts | 33 ++++++--- src/backend/sources/ScrobbleSources.ts | 23 +++--- src/backend/utils/SchemaDocsStaticUtils.ts | 2 +- 5 files changed, 80 insertions(+), 65 deletions(-) diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts index aff9f492..31ea9862 100644 --- a/src/backend/common/infrastructure/config/client/clients.ts +++ b/src/backend/common/infrastructure/config/client/clients.ts @@ -9,6 +9,7 @@ import {librefmClientAIOConfigSchema, librefmClientConfigSchema, type LibrefmCli import {discordClientAIOConfigSchema, discordClientConfigSchema, type DiscordClientAIOConfig, type DiscordClientConfig} from "./discord.ts"; import type { CommonClientConfig } from "./index.ts"; import type { ClientType } from "../../../../../core/Atomic.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; export const clientConfigSchema = z.union([ malojaClientConfigSchema, @@ -36,50 +37,37 @@ export const clientAIOConfigSchema = z.union([ export type ClientAIOConfig = z.infer; -export interface ClientTypeConfigMap extends Record { - maloja: MalojaClientConfig, - lastfm: LastfmClientConfig, - librefm: LibrefmClientConfig, - listenbrainz: ListenBrainzClientConfig, - koito: KoitoClientConfig, - tealfm: TealClientConfig, - rocksky: RockSkyClientConfig, - discord: DiscordClientConfig +export interface ClientTypeConfigMap extends Record { + maloja: [MalojaClientConfig,MalojaClientAIOConfig], + lastfm: [LastfmClientConfig,LastfmClientAIOConfig], + librefm: [LibrefmClientConfig,LibrefmClientAIOConfig], + listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig], + koito: [KoitoClientConfig,KoitoClientAIOConfig], + tealfm: [TealClientConfig,TealClientAIOConfig], + rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig], + discord: [DiscordClientConfig,DiscordClientAIOConfig] } -export 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 clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: [z.ZodType,z.ZodType] } = { + maloja: [malojaClientConfigSchema,malojaClientAIOConfigSchema], + lastfm: [lastfmClientConfigSchema,lastfmClientAIOConfigSchema], + librefm: [librefmClientConfigSchema,librefmClientAIOConfigSchema], + listenbrainz: [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema], + koito: [koitoClientConfigSchema,koitoClientAIOConfigSchema], + tealfm: [tealClientConfigSchema,tealClientAIOConfigSchema], + rocksky: [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema], + discord: [discordClientConfigSchema,discordClientAIOConfigSchema] } -export const validateClientJson = (clientType: T, json: object): ClientTypeConfigMap[T] => clientConfigSchemaMap[clientType].parse(json); - -export interface ClientTypeAIOConfigMap extends Record { - maloja: MalojaClientAIOConfig, - lastfm: LastfmClientAIOConfig, - librefm: LibrefmClientAIOConfig, - listenbrainz: ListenBrainzClientAIOConfig, - koito: KoitoClientAIOConfig, - tealfm: TealClientAIOConfig, - rocksky: RockSkyClientAIOConfig, - discord: DiscordClientAIOConfig -} - -export const clientAIOConfigSchemaMap: { [K in keyof ClientTypeAIOConfigMap]: z.ZodType } = { - maloja: malojaClientAIOConfigSchema, - lastfm: lastfmClientAIOConfigSchema, - librefm: librefmClientAIOConfigSchema, - listenbrainz: listenBrainzClientAIOConfigSchema, - koito: koitoClientAIOConfigSchema, - tealfm: tealClientAIOConfigSchema, - rocksky: rockSkyClientAIOConfigSchema, - discord: discordClientAIOConfigSchema +export const validateClientJson = (clientType: T, json: object): ClientTypeConfigMap[T][0] => { + if(clientConfigSchemaMap[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return clientConfigSchemaMap[clientType][0].parse(json); } - -export const validateClientAIOJson = (clientType: T, json: object): ClientTypeAIOConfigMap[T] => clientAIOConfigSchemaMap[clientType].parse(json); \ No newline at end of file +export const validateClientAIOJson = (clientType: T, json: object): ClientTypeConfigMap[T][1] => { + if(clientConfigSchemaMap[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return clientConfigSchemaMap[clientType][1].parse(json); +} \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/source/sources.ts b/src/backend/common/infrastructure/config/source/sources.ts index aa0df628..3350d3f2 100644 --- a/src/backend/common/infrastructure/config/source/sources.ts +++ b/src/backend/common/infrastructure/config/source/sources.ts @@ -31,6 +31,7 @@ import {sonosSourceAIOConfigSchema, sonosSourceConfigSchema, type SonosSourceAIO import {appleMusicSourceAIOConfigSchema, appleMusicSourceConfigSchema, type AppleMusicSourceAIOConfig, type AppleMusicSourceConfig} from "./applemusic.ts"; import type { SourceType } from "../../../../../core/Atomic.ts"; import type { CommonSourceConfig } from "./index.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; export const sourceConfigSchema = z.union([ spotifySourceConfigSchema, @@ -168,5 +169,15 @@ export const sourceConfigSchemaMap: { [K in keyof SourceTypeConfigMap]: [z.ZodTy applemusic: [appleMusicSourceConfigSchema, appleMusicSourceAIOConfigSchema] }; -export const validateSourceJson = (sourceType: T, json: object): SourceTypeConfigMap[T][0] => sourceConfigSchemaMap[sourceType][0].parse(json); -export const validateSourceAIOJson = (sourceType: T, json: object): SourceTypeConfigMap[T][1] => sourceConfigSchemaMap[sourceType][1].parse(json); \ No newline at end of file +export const validateSourceJson = (sourceType: T, json: object): SourceTypeConfigMap[T][0] => { + if(sourceConfigSchemaMap[sourceType] === undefined) { + throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); + } + return sourceConfigSchemaMap[sourceType][0].parse(json) +}; +export const validateSourceAIOJson = (sourceType: T, json: object): SourceTypeConfigMap[T][1] => { + if(sourceConfigSchemaMap[sourceType] === undefined) { + throw new SimpleError(`No Source has a 'type' of '${sourceType}'`); + } + return sourceConfigSchemaMap[sourceType][1].parse(json) +}; \ No newline at end of file diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index bdabe6e1..85b89c84 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -6,7 +6,7 @@ import type {ConfigMeta, InternalConfig, InternalConfigOptional, SourceIdentifie import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; -import {aioClientRelaxedConfigSchema} from "../common/infrastructure/config/aioConfig.ts"; +import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig} from "../common/infrastructure/config/aioConfig.ts"; import {validateClientAIOJson, validateClientJson, type ClientAIOConfig} 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"; @@ -119,7 +119,18 @@ export default class ScrobbleClients { let clientDefaults = {}; if (configFile !== undefined) { - const aioConfig = aioClientRelaxedConfigSchema.parse(configFile); // await validateJson('client', configFile, 'AIOClientRelaxedConfig', this.logger); + let aioConfig: AIOClientRelaxedConfig; + try { + aioConfig = aioClientRelaxedConfigSchema.parse(configFile); + } catch (e) { + const msg = `Validation error occurred while trying to parse 'config.json' for Client data/options`; + if(e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(new Error(msg, {cause: e})); + } + return; + } const { clients: mainConfigClientConfigs = [], clientDefaults: cd = {}, @@ -135,23 +146,23 @@ export default class ScrobbleClients { this.logger.error(invalidMsgType); continue; } - if(!isClientType(c.type.toLocaleLowerCase())) { - const invalidTypeMsg = `Client config ${index + 1} (${name}) in config.json has an invalid client type of '${c.type}'. Must be one of ${clientTypes.join(' | ')}`; - //this.emitter.emit('error', new Error(invalidTypeMsg)); - this.logger.error(invalidTypeMsg); - continue; - } if(c.configureAs === 'source') { this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a source.`); continue; } let validatedConfig: ClientAIOConfig; try { - validatedConfig = await validateClientAIOJson(c.type.toLocaleLowerCase() as ClientType, c) // validateJson('client', c, this.getSchemaByType(c.type.toLocaleLowerCase() as ClientType), this.logger); + validatedConfig = await validateClientAIOJson(c.type.toLocaleLowerCase() as ClientType, c); } catch (e) { - const err = new Error(`Client config ${index + 1} (${c.type} - ${name}) in config.json is invalid and will not be used.`, {cause: e}); + const msg = `Client config ${index + 1} (${c.type} - ${name}) in config.json is invalid and will not be used.`; + const err = new Error(msg, {cause: e}); this.emitter.emit('error', err); - this.logger.error(err); + // pretty print error if its a zod error + if(e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(err); + } continue; } configs.push({...validatedConfig, diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 62548a77..de9c10d4 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -5,7 +5,7 @@ import type {ConfigMeta, ConfigureAsSource, InternalConfig, InternalConfigOption import { clientTypes, isSourceType } from "../../core/Atomic.ts"; import { sourceTypes } from "../../core/Atomic.ts"; import type {ClientType, SourceType} from "../../core/Atomic.ts"; -import {aioSourceRelaxedConfigSchema, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; +import {aioSourceRelaxedConfigSchema, type AIOSourceRelaxedConfig, type SourceDefaults} from "../common/infrastructure/config/aioConfig.ts"; import type {AzuracastData, AzuracastSourceConfig} from "../common/infrastructure/config/source/azuracast.ts"; import type {ChromecastData, ChromecastSourceConfig} from "../common/infrastructure/config/source/chromecast.ts"; import type {DeezerSourceConfig, DeezerInternalSourceConfig, DeezerCompatConfig} from "../common/infrastructure/config/source/deezer.ts"; @@ -162,7 +162,18 @@ export default class ScrobbleSources { let sourceDefaults: SourceDefaults; if (configFile !== undefined) { - const aioConfig = aioSourceRelaxedConfigSchema.parse(configFile); // await validateJson('source', configFile, 'AIOSourceRelaxedConfig', this.logger); + let aioConfig: AIOSourceRelaxedConfig; + try { + aioConfig = aioSourceRelaxedConfigSchema.parse(configFile); + } catch (e) { + const msg = `Validation error occurred while trying to parse 'config.json' for Source data/options`; + if(e instanceof ZodError) { + this.logger.error(`${msg}:\n${prettifyError(e)}`); + } else { + this.logger.error(new Error(msg, {cause: e})); + } + return; + } const { sources: mainConfigSourcesConfigs = [], sourceDefaults: sd = {}, @@ -179,12 +190,6 @@ export default class ScrobbleSources { this.logger.error(invalidMsgType); continue; } - if(!isSourceType(c.type.toLocaleLowerCase())) { - const invalidMsgType = `Source config ${index + 1} (${name}) in config.json has an invalid source "type" of "${c.type}". Must be one of ${sourceTypes.join(' | ')}`; - this.emitter.emit('error', new Error(invalidMsgType)); - this.logger.error(invalidMsgType); - continue; - } if(clientTypes.includes(c.type.toLocaleLowerCase() as ClientType) && (c.configureAs !== 'source')) { this.logger.debug(`Skipping config ${index + 1} (${name}) in config.json because it is configured as a client.`); @@ -192,7 +197,7 @@ export default class ScrobbleSources { } let validatedSourceConfig: SourceAIOConfig; try { - validatedSourceConfig = validateSourceAIOJson(c.type.toLocaleLowerCase() as SourceType, c); // validateJson('source', c, this.getSchemaByType(c.type.toLocaleLowerCase() as SourceType), this.logger); + validatedSourceConfig = validateSourceAIOJson(c.type.toLocaleLowerCase() as SourceType, c); } catch (e) { const msg = `Source config ${index + 1} (${c.type} - ${name}) in config.json is invalid and will not be used.`; const err = new Error(msg, {cause: e}); diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 5b039ca4..c2b237e9 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -42,7 +42,7 @@ const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['re const clientEntries = Object.entries(clientConfigSchemaMap); for(const [k,v] of clientEntries) { - writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(v)))); + writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(v[0])))); } const sourcesEntries = Object.entries(sourceConfigSchemaMap); -- 2.51.2