diff --git a/docsite/docs/configuration/clients/_env_configs/_maloja.md b/docsite/docs/configuration/clients/_env_configs/_maloja.md new file mode 100644 index 00000000..33d0628a --- /dev/null +++ b/docsite/docs/configuration/clients/_env_configs/_maloja.md @@ -0,0 +1,7 @@ +| Environmental Variable | Type | Default | Description | +| ---------------------- | ------- | -------------------- | ----------------------------- | +| _**`MALOJA_ID`**_ | string | | A globally unique ID | +| `MALOJA_NAME` | string | Value of `MALOJA_ID` | A vanity name | +| `MALOJA_ENABLE` | boolean | | | +| _**`MALOJA_URL`**_ | string | | Base URL of your installation | +| `MALOJA_API_KEY` | string | fsdfsd | | \ No newline at end of file diff --git a/docsite/docs/configuration/clients/maloja.mdx b/docsite/docs/configuration/clients/maloja.mdx index 1d5edc11..9c11f3b4 100644 --- a/docsite/docs/configuration/clients/maloja.mdx +++ b/docsite/docs/configuration/clients/maloja.mdx @@ -8,6 +8,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import CodeBlock from '@theme/CodeBlock'; import JsonConfig from '!!raw-loader!@site/../config/maloja.json.example'; +import ENVConfig from "@site/docs/configuration/clients/_env_configs/_maloja.md" Setup a [Maloja server](https://github.com/krateng/maloja?tab=readme-ov-file#how-to-install) if you have not already done this. @@ -38,10 +39,5 @@ Finally, add the Maloja server URL and API Key to the configuration type you cho ## Configuration - | Environmental Variable | Required? | Default | Description | - | ---------------------- | --------- | ------- | ------------------------------- | - | `MALOJA_ID` | Yes | | A unique ID | - | `MALOJA_URL` | Yes | | Base URL of your installation | - | `MALOJA_API_KEY` | Yes | | Api Key | - | `MALOJA_NAME` | No | | A vanity name different than ID | + \ No newline at end of file diff --git a/docsite/src/components/GenericConfiguration.mdx b/docsite/src/components/GenericConfiguration.mdx index 15342f80..863853c6 100644 --- a/docsite/src/components/GenericConfiguration.mdx +++ b/docsite/src/components/GenericConfiguration.mdx @@ -11,6 +11,8 @@ import {useWindowSize} from '@docusaurus/theme-common'; {props.children} + + _**Bold/Italic**_ = Required diff --git a/package-lock.json b/package-lock.json index ee166195..75d9ff20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,7 @@ "git-cliff": "^2.12.0", "globals": "^17.7.0", "ky": "^2.0.2", + "markdown-table": "^3.0.4", "mocha": "^10.3.0", "mockdate": "^3.0.5", "msw": "^2.14.6", @@ -15190,6 +15191,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 2877ad83..bb3ec336 100644 --- a/package.json +++ b/package.json @@ -222,6 +222,7 @@ "git-cliff": "^2.12.0", "globals": "^17.7.0", "ky": "^2.0.2", + "markdown-table": "^3.0.4", "mocha": "^10.3.0", "mockdate": "^3.0.5", "msw": "^2.14.6", diff --git a/src/backend/common/infrastructure/config/client/maloja.ts b/src/backend/common/infrastructure/config/client/maloja.ts index 138cb67a..31fe2828 100644 --- a/src/backend/common/infrastructure/config/client/maloja.ts +++ b/src/backend/common/infrastructure/config/client/maloja.ts @@ -31,6 +31,15 @@ export const malojaClientDataSchema = malojaDataSchema.extend(commonClientDataSc export type MalojaClientData = z.infer; +const envDataSchema = z.object({ + MALOJA_URL: z.string().meta({description: 'Base URL of your installation'}), + MALOJA_API_KEY: z.string().default('fsdfsd') +}); + +export const envSchemas = { + data: envDataSchema +}; + export const malojaClientConfigSchema = z.object({ ...commonClientConfigSchema.shape, /** @@ -54,4 +63,4 @@ export const malojaClientAIOConfigSchema = z.object({ type: z.literal('maloja'), }).meta({title: 'Maloja'}); -export type MalojaClientAIOConfig = z.infer; +export type MalojaClientAIOConfig = z.infer; \ No newline at end of file diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 3f3ed479..ab940df2 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -1,5 +1,7 @@ import { stripIndents } from "common-tags"; import * as z from "zod"; +import type { PlayTransformHooks, ExternalMetadataTerm } from "../../../../core/Transform.ts"; +import type { CommonClientOptions } from "./client/index.ts"; export const commonConfigPrimitivesSchema = z.object({ name: z.string().optional(), @@ -140,4 +142,65 @@ export const monitorOptionsSchema = z.object({ description: 'Set the default behavior for wether this component should automatically monitor any activity, or scrobble, it encounters' }) }) -export type MonitorOptions = z.infer; \ No newline at end of file +export type MonitorOptions = z.infer; + +export const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { + + const env = process.env[`${prefix}_TRANSFORMS`]; + if (env === undefined || env.trim() === '') { + return existing; + } + + const popts: PlayTransformHooks = { + preCompare: [] + }; + for (const p of env.split(',').map(x => x.trim().toLocaleLowerCase())) { + switch (p) { + case 'native': + popts.preCompare.push({ type: 'native' }); + break; + case 'musicbrainz': + popts.preCompare.push({ type: 'musicbrainz' }); + break; + } + } + + // @ts-expect-error T is fine + return { + ...(existing || {}), + playTransform: popts + }; +}; + + +type CommonComponentEnvShape = { + [K in `${T}_ID`]: z.ZodString +} & { + [K in `${T}_NAME`]: z.ZodOptional +} & { + [K in `${T}_ENABLE`]: z.ZodOptional> +}; + +export const generateCommonComponentEnvConfigSchema = (prefix: T) => + z.object({ + [`${prefix}_ID`]: z.string().meta({description: 'A globally unique ID'}), + [`${prefix}_NAME`]: z.string().optional().meta({description: 'A vanity name', default: `Value of \`${prefix}_ID\``}), + [`${prefix}_ENABLE`]: z.stringbool().optional() + } as CommonComponentEnvShape); + +type CommonComponentEnvConfigParsed = { + [K in `${T}_ID`]: string +} & { + [K in `${T}_NAME`]?: string +} & { + [K in `${T}_ENABLE`]?: boolean +}; + +export const commonComponentEnvConfigToConfigPrimitives = (prefix: T, envConfig: CommonComponentEnvConfigParsed): CommonConfigPrimitives => { + const raw = envConfig as Record; + return { + id: raw[`${prefix}_ID`] as string, + name: raw[`${prefix}_NAME`] as string | undefined, + enable: raw[`${prefix}_ENABLE`] as boolean | undefined + }; +}; diff --git a/src/backend/scrobblers/ScrobbleClients.ts b/src/backend/scrobblers/ScrobbleClients.ts index 85b89c84..f8a373e1 100644 --- a/src/backend/scrobblers/ScrobbleClients.ts +++ b/src/backend/scrobblers/ScrobbleClients.ts @@ -20,14 +20,13 @@ import type AbstractScrobbleClient from "./AbstractScrobbleClient.ts"; import type {KoitoClientConfig, KoitoData} from '../common/infrastructure/config/client/koito.ts'; import type {TealClientConfig, TealData} from '../common/infrastructure/config/client/tealfm.ts'; import type {RockSkyClientConfig, RockSkyData} from '../common/infrastructure/config/client/rocksky.ts'; -import type {CommonClientOptions} from '../common/infrastructure/config/client/index.ts'; -import type {ExternalMetadataTerm, PlayTransformHooks} from '../../core/Transform.ts'; import type {LibrefmClientConfig, LibrefmData} from '../common/infrastructure/config/client/librefm.ts'; 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'; +import { transformPresetEnv } from '../common/infrastructure/config/common.ts'; type groupedNamedConfigs = {[key: string]: ParsedConfig[]}; @@ -604,33 +603,4 @@ ${sources.join('\n')}`); this.logger.trace(`These clients were filtered from scrobbling from Source '${scrobbleFrom}' => ${excluded.join(' | ')}`); } } -} - -const transformPresetEnv = (prefix: string, existing: T = undefined): undefined | T => { - - const env = process.env[`${prefix}_TRANSFORMS`]; - if(env === undefined || env.trim() === '') { - return existing; - } - - const popts: PlayTransformHooks = { - preCompare: [ - ] - } - for(const p of env.split(',').map(x => x.trim().toLocaleLowerCase())) { - switch(p) { - case 'native': - popts.preCompare.push({type: 'native'}); - break; - case 'musicbrainz': - popts.preCompare.push({type: 'musicbrainz'}); - break; - } - } - - // @ts-expect-error T is fine - return { - ...(existing || {}), - playTransform: popts - }; -} +} \ No newline at end of file diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index c2b237e9..30f61110 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -5,9 +5,36 @@ import { clientConfigSchemaMap } from '../common/infrastructure/config/client/cl import { sourceConfigSchemaMap } from '../common/infrastructure/config/source/sources.ts'; import { projectRootDir } from "../common/infrastructure/Atomic.ts"; import { aioConfigSchema } from "../common/infrastructure/config/aioConfig.ts"; +import { envSchemas } from "../common/infrastructure/config/client/maloja.ts"; +import { generateCommonComponentEnvConfigSchema } from "../common/infrastructure/config/common.ts"; +import { zodObjectToTableColumns } from "./ZodUtils.ts"; +import {markdownTable} from 'markdown-table' mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); +const common = generateCommonComponentEnvConfigSchema('MALOJA'); +const schema = z.object({ + ...common.shape, + ...envSchemas.data.shape +}); +const col = zodObjectToTableColumns(schema, 'out'); + +const mdCols: string[][] = col.map((x) => { + const name = `\`${x.title}\``; + return [ + x.required ? `**${name}**` : name, + x.type, + (x.default ?? '').toString(), + x.description ?? '' + ] +}); +const tableContent = markdownTable([ + ['Environmental Variable', 'Type', 'Default', 'Description'], + ...mdCols +]); + +writeFileSync(resolve(projectRootDir, `docsite/static/schemas/maloja-env-test.md`), tableContent); + const generateSchema = (schema: z.ZodType, reused: z.core.ToJSONSchemaParams['reused'] = 'inline') => z.toJSONSchema(schema, { io: "input", reused, diff --git a/src/backend/utils/ZodUtils.ts b/src/backend/utils/ZodUtils.ts new file mode 100644 index 00000000..33e4a410 --- /dev/null +++ b/src/backend/utils/ZodUtils.ts @@ -0,0 +1,52 @@ +import z from 'zod'; + +export interface TableColumn { + title: string + required: boolean + type: string + default: unknown + description: string | undefined +} + +export type PipeUnwrapDirection = 'in' | 'out'; + +const unwrapZodType = (schema: z.ZodTypeAny, pipeDirection: PipeUnwrapDirection = 'in'): z.ZodTypeAny => { + let current = schema; + while (current instanceof z.ZodOptional || current instanceof z.ZodNullable || current instanceof z.ZodDefault || current instanceof z.ZodPipe) { + current = (current instanceof z.ZodPipe ? current[pipeDirection] : current.unwrap()) as z.ZodTypeAny; + } + return current; +}; + +const getExplicitDefault = (schema: z.ZodTypeAny): unknown => { + let current = schema; + while (true) { + if (current instanceof z.ZodDefault) { + return current.def.defaultValue; + } + if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) { + current = current.unwrap() as z.ZodTypeAny; + continue; + } + return undefined; + } +}; + +/** + * Takes a z.object and returns objects that can be used to build table rows for docs + * + * Based loosely on matejchalk/zod2md + * */ +export const zodObjectToTableColumns = (schema: z.ZodObject, pipeDirection: PipeUnwrapDirection = 'in'): TableColumn[] => + Object.entries(schema.shape).map(([key, value]) => { + const fieldSchema = value as z.ZodTypeAny; + const meta = fieldSchema.meta() as Record | undefined; + + return { + title: (meta?.title as string | undefined) ?? key, + required: !fieldSchema.isOptional(), + type: unwrapZodType(fieldSchema, pipeDirection).def.type, + default: getExplicitDefault(fieldSchema) ?? (meta?.default as unknown), + description: meta?.description as string | undefined + }; + }); \ No newline at end of file