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