From 02ab1571b677594c886f5a9a139589fbd4fc22e1 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Fri, 31 Jul 2026 23:57:59 +0000 Subject: [PATCH] refactor: Convert validation client structures to dynamically import --- .../common/infrastructure/config/aioConfig.ts | 8 -- .../infrastructure/config/client/clients.ts | 56 +++---------- .../config/client/clientsMap.ts | 79 +++++++++++++++++++ src/backend/scrobblers/ScrobbleClients.ts | 8 +- src/backend/sources/ScrobbleSources.ts | 4 +- src/backend/tests/config/config.test.ts | 4 +- src/backend/utils/SchemaDocsStaticUtils.ts | 11 +-- 7 files changed, 102 insertions(+), 68 deletions(-) create mode 100644 src/backend/common/infrastructure/config/client/clientsMap.ts diff --git a/src/backend/common/infrastructure/config/aioConfig.ts b/src/backend/common/infrastructure/config/aioConfig.ts index 630272a6..dcd21126 100644 --- a/src/backend/common/infrastructure/config/aioConfig.ts +++ b/src/backend/common/infrastructure/config/aioConfig.ts @@ -1,5 +1,4 @@ import * as z from "zod"; -import {clientAIOConfigSchema} from "./client/clients.ts"; import {commonClientOptionsSchema} from "./client/index.ts"; import {requestRetryOptionsSchema} from "./common.ts"; import {webhookConfigSchema} from "./health/webhooks.ts"; @@ -118,13 +117,6 @@ export const aioConfigSchema = z.object({ export type AIOConfig = z.infer; -export const aioClientConfigSchema = z.object({ - clientDefaults: requestRetryOptionsSchema.optional(), - clients: z.array(clientAIOConfigSchema).optional(), -}); - -export type AIOClientConfig = z.infer; - export const aioClientRelaxedConfigSchema = z.object({ clientDefaults: requestRetryOptionsSchema.optional(), clients: z.array(z.looseObject({ type: z.string(), configureAs: z.string().optional() })).optional(), diff --git a/src/backend/common/infrastructure/config/client/clients.ts b/src/backend/common/infrastructure/config/client/clients.ts index 9c34eded..cda106cd 100644 --- a/src/backend/common/infrastructure/config/client/clients.ts +++ b/src/backend/common/infrastructure/config/client/clients.ts @@ -1,15 +1,12 @@ import * as z from "zod"; -import {koitoClientAIOConfigSchema, koitoClientConfigSchema, envSchemas as koitoEnvSchemas, type KoitoClientAIOConfig, type KoitoClientConfig} from "./koito.ts"; -import {lastfmClientAIOConfigSchema, lastfmClientConfigSchema, envSchemas as lastfmEnvSchemas, type LastfmClientAIOConfig, type LastfmClientConfig} from "./lastfm.ts"; -import {listenBrainzClientAIOConfigSchema, listenBrainzClientConfigSchema, envSchemas as listenBrainzEnvSchemas, type ListenBrainzClientAIOConfig, type ListenBrainzClientConfig} from "./listenbrainz.ts"; -import {malojaClientAIOConfigSchema, malojaClientConfigSchema, envSchemas as malojaEnvSchemas, type MalojaClientAIOConfig, type MalojaClientConfig} from "./maloja.ts"; -import {tealClientAIOConfigSchema, tealClientConfigSchema, envSchemas as tealEnvSchemas, type TealClientAIOConfig, type TealClientConfig} from "./tealfm.ts"; -import {rockSkyClientAIOConfigSchema, rockSkyClientConfigSchema, envSchemas as rockSkyEnvSchemas, type RockSkyClientAIOConfig, type RockSkyClientConfig} from "./rocksky.ts"; -import {librefmClientAIOConfigSchema, librefmClientConfigSchema, envSchemas as librefmEnvSchemas, type LibrefmClientAIOConfig, type LibrefmClientConfig} from "./librefm.ts"; -import {discordClientAIOConfigSchema, discordClientConfigSchema, envSchemas as discordEnvSchemas, type DiscordClientAIOConfig, type DiscordClientConfig} from "./discord.ts"; -import type { CommonClientConfig, EnvClientSchema } from "./index.ts"; -import type { ClientType } from "../../../../../core/Atomic.ts"; -import { SimpleError } from "../../../errors/MSErrors.ts"; +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"; export const clientConfigSchema = z.union([ malojaClientConfigSchema, @@ -35,39 +32,4 @@ export const clientAIOConfigSchema = z.union([ discordClientAIOConfigSchema, ]); -export type ClientAIOConfig = z.infer; - -export interface ClientTypeConfigMap extends Record>]> { - maloja: [MalojaClientConfig,MalojaClientAIOConfig, Partial>], - lastfm: [LastfmClientConfig,LastfmClientAIOConfig, Partial>], - librefm: [LibrefmClientConfig,LibrefmClientAIOConfig, Partial>], - listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig, Partial>], - koito: [KoitoClientConfig,KoitoClientAIOConfig, Partial>], - tealfm: [TealClientConfig,TealClientAIOConfig, Partial>], - rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig, Partial>], - discord: [DiscordClientConfig,DiscordClientAIOConfig, Partial>] -} - -export const clientConfigSchemaMap: { [K in keyof ClientTypeConfigMap]: [z.ZodType,z.ZodType,EnvClientSchema] } = { - maloja: [malojaClientConfigSchema,malojaClientAIOConfigSchema,malojaEnvSchemas], - lastfm: [lastfmClientConfigSchema,lastfmClientAIOConfigSchema,lastfmEnvSchemas], - librefm: [librefmClientConfigSchema,librefmClientAIOConfigSchema,librefmEnvSchemas], - listenbrainz: [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema,listenBrainzEnvSchemas], - koito: [koitoClientConfigSchema,koitoClientAIOConfigSchema,koitoEnvSchemas], - tealfm: [tealClientConfigSchema,tealClientAIOConfigSchema,tealEnvSchemas], - rocksky: [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema,rockSkyEnvSchemas], - discord: [discordClientConfigSchema,discordClientAIOConfigSchema,discordEnvSchemas] -} - -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): 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 +export type ClientAIOConfig = z.infer; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/client/clientsMap.ts b/src/backend/common/infrastructure/config/client/clientsMap.ts new file mode 100644 index 00000000..2d64ef2f --- /dev/null +++ b/src/backend/common/infrastructure/config/client/clientsMap.ts @@ -0,0 +1,79 @@ +import type { KoitoClientAIOConfig, KoitoClientConfig} from "./koito.ts"; +import type { LastfmClientAIOConfig, LastfmClientConfig} from "./lastfm.ts"; +import type { ListenBrainzClientAIOConfig, ListenBrainzClientConfig} from "./listenbrainz.ts"; +import type { MalojaClientAIOConfig, MalojaClientConfig} from "./maloja.ts"; +import type { TealClientAIOConfig, TealClientConfig} from "./tealfm.ts"; +import type { RockSkyClientAIOConfig, RockSkyClientConfig} from "./rocksky.ts"; +import type { LibrefmClientAIOConfig, LibrefmClientConfig} from "./librefm.ts"; +import type { DiscordClientAIOConfig, DiscordClientConfig} from "./discord.ts"; +import type { ClientAIOConfig } from "./clients.ts"; +import type { CommonClientConfig, EnvClientSchema } from "./index.ts"; +import type {ZodType, ZodObject} from 'zod'; +import type { ClientType } from "../../../../../core/Atomic.ts"; +import { SimpleError } from "../../../errors/MSErrors.ts"; + + +export interface ClientTypeConfigMap extends Record>]> { + maloja: [MalojaClientConfig,MalojaClientAIOConfig, Partial>], + lastfm: [LastfmClientConfig,LastfmClientAIOConfig, Partial>], + librefm: [LibrefmClientConfig,LibrefmClientAIOConfig, Partial>], + listenbrainz: [ListenBrainzClientConfig,ListenBrainzClientAIOConfig, Partial>], + koito: [KoitoClientConfig,KoitoClientAIOConfig, Partial>], + tealfm: [TealClientConfig,TealClientAIOConfig, Partial>], + rocksky: [RockSkyClientConfig,RockSkyClientAIOConfig, Partial>], + discord: [DiscordClientConfig,DiscordClientAIOConfig, Partial>] +} + +export const clientConfigSchemaMapAsync: { [K in keyof ClientTypeConfigMap]:() => Promise<[ZodType,ZodType,EnvClientSchema]> } = { + maloja: async() => { + const {malojaClientConfigSchema, malojaClientAIOConfigSchema, envSchemas} = (await import('./maloja.ts')); + return [malojaClientConfigSchema,malojaClientAIOConfigSchema,envSchemas] + }, + lastfm: async() => { + const {lastfmClientConfigSchema, lastfmClientAIOConfigSchema, envSchemas} = (await import('./lastfm.ts')); + return [lastfmClientConfigSchema,lastfmClientAIOConfigSchema,envSchemas] + }, + librefm: async() => { + const {librefmClientConfigSchema, librefmClientAIOConfigSchema, envSchemas} = (await import('./librefm.ts')); + return [librefmClientConfigSchema,librefmClientAIOConfigSchema,envSchemas] + }, + listenbrainz: async() => { + const {listenBrainzClientConfigSchema, listenBrainzClientAIOConfigSchema, envSchemas} = (await import('./listenbrainz.ts')); + return [listenBrainzClientConfigSchema,listenBrainzClientAIOConfigSchema,envSchemas] + }, + koito: async() => { + const {koitoClientConfigSchema, koitoClientAIOConfigSchema, envSchemas} = (await import('./koito.ts')); + return [koitoClientConfigSchema,koitoClientAIOConfigSchema,envSchemas] + }, + tealfm: async() => { + const {tealClientConfigSchema, tealClientAIOConfigSchema, envSchemas} = (await import('./tealfm.ts')); + return [tealClientConfigSchema,tealClientAIOConfigSchema,envSchemas] + }, + rocksky: async() => { + const {rockSkyClientConfigSchema, rockSkyClientAIOConfigSchema, envSchemas} = (await import('./rocksky.ts')); + return [rockSkyClientConfigSchema,rockSkyClientAIOConfigSchema,envSchemas] + }, + discord: async() => { + const {discordClientConfigSchema, discordClientAIOConfigSchema, envSchemas} = (await import('./discord.ts')); + return [discordClientConfigSchema,discordClientAIOConfigSchema,envSchemas] + } +} + +export const validateClientJson = async (clientType: T, json: object): Promise => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[0].parse(json); +} +export const validateClientAIOJson = async (clientType: T, json: object): Promise => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[1].parse(json); +} +export const getClientEnvSchema = async (clientType: T): Promise> => { + if(clientConfigSchemaMapAsync[clientType] === undefined) { + throw new SimpleError(`No Client has a 'type' of '${clientType}'`); + } + return (await clientConfigSchemaMapAsync[clientType]())[2]; +}; \ No newline at end of file diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index c4763cc6..b95fa92f 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -7,7 +7,6 @@ import { isClientType } from '../../core/Atomic.ts'; import { clientTypes } from "../../core/Atomic.ts"; import type {ClientType} from "../../core/Atomic.ts"; import {aioClientRelaxedConfigSchema, type AIOClientRelaxedConfig, type ClientDefaults} from "../common/infrastructure/config/aioConfig.ts"; -import {clientConfigSchemaMap, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap} from "../common/infrastructure/config/client/clients.ts"; import type { WildcardEmitter } from "../common/WildcardEmitter.ts"; import { pick } from '../../core/DataUtils.ts'; import { readJson } from '../utils/DataUtils.ts'; @@ -18,6 +17,7 @@ import { normalizeStr, type StringNormalizationOptions } from '../utils/StringUt import { prettifyError, ZodError } from 'zod'; import { commonComponentEnvConfigToConfigPrimitives, generateCommonComponentEnvConfigSchema, transformPresetEnv, type CommonConfigPrimitives } from '../common/infrastructure/config/common.ts'; import type { CommonClientConfig } from '../common/infrastructure/config/client/index.ts'; +import { getClientEnvSchema, validateClientAIOJson, validateClientJson, type ClientTypeConfigMap } from '../common/infrastructure/config/client/clientsMap.ts'; type UnparsedConfig = {config: object, type: ClientType, source?: 'file' | 'aio' | 'env', pos: string}; @@ -198,7 +198,7 @@ export default class ScrobbleClients { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const envSchema = clientConfigSchemaMap[clientType][2]; + const envSchema = await getClientEnvSchema(clientType); const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); const parsed = primitiveSchema.parse(entry.config); const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); @@ -222,7 +222,7 @@ export default class ScrobbleClients { this.logger.debug(`Skipping ${clientType} Config ${entry.source} ${entry.pos} because it is configured as a Source`); continue; } - const parsed = entry.source === 'file' ? validateClientJson(entry.type, entry.config) : validateClientAIOJson(entry.type, entry.config); + const parsed = entry.source === 'file' ? (await validateClientJson(entry.type, entry.config)) : (await validateClientAIOJson(entry.type, entry.config)); parsedConfig = { ...parsed, source: sourceStr @@ -259,7 +259,7 @@ export default class ScrobbleClients { ) => { for (const s of strongConfigs) { try { - const config = clientConfigSchemaMap[clientType][0].parse(s); + const config = await validateClientJson(clientType, s); const compositeOptions = { ...clientDefaults, ...config.options }; const newClient = new Ctor(...buildArgs(config, compositeOptions)); newClient.logger.info(`Client added from ${s.source}`); diff --git a/src/backend/sources/ScrobbleSources.ts b/src/backend/sources/ScrobbleSources.ts index 837c567a..b93b2837 100644 --- a/src/backend/sources/ScrobbleSources.ts +++ b/src/backend/sources/ScrobbleSources.ts @@ -218,7 +218,7 @@ export default class ScrobbleSources { const sourceStr = `${entry.source} ${entry.pos}`; switch (entry.source) { case 'env': { - const envSchema = await getSourceEnvSchema(configType); // sourceConfigSchemaMap[configType][2]; + const envSchema = await getSourceEnvSchema(configType); const primitiveSchema = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); const parsed = primitiveSchema.parse(entry.config); const primitives: CommonConfigPrimitives = commonComponentEnvConfigToConfigPrimitives(envSchema.prefix.toUpperCase(), parsed); @@ -278,7 +278,7 @@ export default class ScrobbleSources { ) => { for (const s of strongConfigs) { try { - const config = await validateSourceJson(sourceType, s); // sourceConfigSchemaMap[sourceType][0].parse(s); + const config = await validateSourceJson(sourceType, s); const compositeOptions = { ...defaults, ...config.options }; const newComponent = new Ctor(config.name, { ...config, options: compositeOptions }, this.internalConfig, this.emitter); newComponent.logger.info(`Source added from ${s.source}`); diff --git a/src/backend/tests/config/config.test.ts b/src/backend/tests/config/config.test.ts index b23d6b57..650b647d 100644 --- a/src/backend/tests/config/config.test.ts +++ b/src/backend/tests/config/config.test.ts @@ -15,7 +15,7 @@ import { difference } from '../../utils.ts'; import { validateSourceJson } from '../../common/infrastructure/config/source/sourcesMap.ts'; import { readJson } from '../../utils/DataUtils.ts'; import { prettifyError, ZodError } from 'zod'; -import { validateClientJson } from '../../common/infrastructure/config/client/clients.ts'; +import { validateClientJson } from '../../common/infrastructure/config/client/clientsMap.ts'; chai.use(asPromised); @@ -122,7 +122,7 @@ describe('Sample Configs', function () { fileContents = fileContents.filter(x => x.configureAs === undefined || x.configureAs === 'client'); for (const [i,rawConf] of fileContents.entries()) { try { - validateClientJson(componentType, rawConf); + await validateClientJson(componentType, rawConf); } catch (e) { if(e instanceof ZodError) { expect.fail(`Validation failed for config entry ${i}:\n${prettifyError(e)}`); diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index 616a98c5..37985598 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -1,7 +1,7 @@ import { writeFileSync, mkdirSync } from "node:fs"; import { resolve } from "path"; import * as z from 'zod'; -import { clientConfigSchemaMap } from '../common/infrastructure/config/client/clients.ts'; +import { clientConfigSchemaMapAsync } from '../common/infrastructure/config/client/clientsMap.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; @@ -54,13 +54,14 @@ const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['re } }) -const clientEntries = Object.entries(clientConfigSchemaMap); +const clientEntries = Object.entries(clientConfigSchemaMapAsync); for(const [k,v] of clientEntries) { - writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(v[0])))); + const [fileSchema, aioSchema, envSchemas] = await v(); + writeFileSync(resolve(projectRootDir, `docsite/static/schemas/${k}-client.json`), JSON.stringify(generateSchema(z.array(fileSchema)))); - const envSchema = v[2]; + const envSchema = envSchemas; const common = generateCommonComponentEnvConfigSchema(envSchema.prefix.toUpperCase()); - const col = zodObjectToTableColumns(z.object({...common.shape,...v[2].env.shape}), 'out'); + const col = zodObjectToTableColumns(z.object({...common.shape,...envSchema.env.shape}), 'out'); const tableContent = markdownTable([ ['Environmental Variable', 'Type', 'Default', 'Description'], ...mdCols(col) -- 2.51.2