diff --git a/src/backend/common/infrastructure/config/common.ts b/src/backend/common/infrastructure/config/common.ts index 40898142..86ff3825 100644 --- a/src/backend/common/infrastructure/config/common.ts +++ b/src/backend/common/infrastructure/config/common.ts @@ -1,3 +1,4 @@ +import { stripIndents } from "common-tags"; import * as z from "zod"; export const commonConfigPrimitivesSchema = z.object({ @@ -47,7 +48,8 @@ export const requestRetryOptionsSchema = z.object({ * @examples [1] * */ maxRequestRetries: z.number().optional().meta({ - description: "default # of http request retries a source/client can make before error is thrown", + description: stripIndents`default # of http **request** retries a source/client can make before error is thrown + a test`, default: 1, examples: [1] }), @@ -62,7 +64,7 @@ export const requestRetryOptionsSchema = z.object({ default: 1.5, examples: [1.5] }) -}); +}).meta({title: 'RequestRetryOptions'}); export type RequestRetryOptions = z.infer; diff --git a/src/backend/tests/component/transformers.test.ts b/src/backend/tests/component/transformers.test.ts index 64ebe4a2..52ecfc03 100644 --- a/src/backend/tests/component/transformers.test.ts +++ b/src/backend/tests/component/transformers.test.ts @@ -4,7 +4,7 @@ import asPromised from 'chai-as-promised'; import { describe, it } from 'mocha'; import AbstractComponent, { type AbstractComponentConfig } from "../../common/AbstractComponent.ts"; -import { type ConditionalSearchAndReplaceRegExp, TRANSFORM_HOOK } from "../../../core/Transform.ts"; +import { type ConditionalSearchAndReplaceRegExp, type StageTypeMetadata, TRANSFORM_HOOK } from "../../../core/Transform.ts"; import { isConditionalSearchAndReplace } from "../../utils/PlayTransformUtils.ts"; import { generateArtistsStr, generatePlay } from "../../../core/tests/utils/PlayTestUtils.ts"; @@ -89,7 +89,9 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + // @ts-expect-error no type named test type: "test", + // @ts-expect-error stage is untyped title: ['something'] } } @@ -106,25 +108,6 @@ describe('Play Transforms', function () { describe('User Stage Parsing', function () { - it(`Assumes user 'type' if no type is present`, function () { - component.config = { - options: { - playTransform: { - preCompare: { - title: ['something'] - } - } - } - } - - component.buildTransformRules(); - - expect(component.transformRules.preCompare).to.be.an('array'); - expect(component.transformRules.preCompare).to.be.length(1); - expect(component.transformRules.preCompare).to.have.nested.property('0.type'); - expect(component.transformRules.preCompare[0].type).eq('user'); - }); - it(`Allows user 'type'`, function () { component.config = { options: { @@ -151,9 +134,11 @@ describe('Play Transforms', function () { playTransform: { preCompare: [ { + type: 'user', title: ['something'] }, { + type: 'user', title: ['something else'] } ] @@ -174,6 +159,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: ['something'] } } @@ -193,6 +179,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: ['something'] } } @@ -215,6 +202,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: [ { @@ -250,7 +238,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { - type: t, + type: t as StageTypeMetadata, title: true } } @@ -286,6 +274,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: ['something'] } } @@ -303,6 +292,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: ['something', 'cool'] } } @@ -320,6 +310,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: [ { search: '/(cool )(some)(thing)/i', @@ -343,6 +334,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', artists: [ { search: '/(.*?)(\\s*\\/\\s*)(.*$)/i', @@ -366,6 +358,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', title: ['something'] } } @@ -383,6 +376,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', album: ['something'] } } @@ -400,6 +394,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', artists: ['something'] } } @@ -477,6 +472,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', when: [ { album: "Has This" @@ -500,6 +496,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', when: [ { album: "Has This" @@ -525,6 +522,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', artists: [ { search: "something", @@ -553,6 +551,7 @@ describe('Play Transforms', function () { options: { playTransform: { preCompare: { + type: 'user', artists: [ { search: "something", @@ -587,6 +586,7 @@ describe('Play Transforms', function () { playTransform: { preCompare: [ { + type: 'user', title: [ { search: "something", @@ -595,6 +595,7 @@ describe('Play Transforms', function () { ] }, { + type: 'user', title: [ { search: "another else", @@ -619,6 +620,7 @@ describe('Play Transforms', function () { playTransform: { preCompare: [ { + type: 'user', title: [ { search: "something", @@ -653,6 +655,7 @@ describe('Play Transforms', function () { playTransform: { preCompare: [ { + type: 'user', name: "barChange", title: [ { diff --git a/src/backend/tests/scrobbler/scrobblers.test.ts b/src/backend/tests/scrobbler/scrobblers.test.ts index f5ee956c..7560bda9 100644 --- a/src/backend/tests/scrobbler/scrobblers.test.ts +++ b/src/backend/tests/scrobbler/scrobblers.test.ts @@ -669,6 +669,7 @@ describe('Scrobble client uses transform plays correctly', function() { testScrobbler.config.options = { playTransform: { postCompare: { + type: 'user', title: [ 'cool' ] @@ -695,6 +696,7 @@ describe('Scrobble client uses transform plays correctly', function() { playTransform: { compare: { candidate: { + type: 'user', title: [ 'hugely cool and very different track' ] @@ -718,6 +720,7 @@ describe('Scrobble client uses transform plays correctly', function() { playTransform: { compare: { existing: { + type: 'user', title: [ 'hugely cool and very different track' ] diff --git a/src/backend/tests/source/source.test.ts b/src/backend/tests/source/source.test.ts index 0f7fc818..800fe161 100644 --- a/src/backend/tests/source/source.test.ts +++ b/src/backend/tests/source/source.test.ts @@ -53,6 +53,7 @@ describe('Sources use transform plays correctly', function () { source.config.options = { playTransform: { preCompare: { + type: 'user', title: [ { search: 'cool', @@ -76,6 +77,7 @@ describe('Sources use transform plays correctly', function () { source.config.options = { playTransform: { postCompare: { + type: 'user', title: [ { search: 'cool', @@ -106,6 +108,7 @@ describe('Sources use transform plays correctly', function () { playTransform: { compare: { existing: { + type: 'user', title: [ { search: 'hugely cool and very different track', @@ -133,6 +136,7 @@ describe('Sources use transform plays correctly', function () { playTransform: { compare: { candidate: { + type: 'user', title: [ { search: 'hugely cool and very different track', diff --git a/src/backend/utils/SchemaDocsStaticUtils.ts b/src/backend/utils/SchemaDocsStaticUtils.ts index e251e517..237dfc55 100644 --- a/src/backend/utils/SchemaDocsStaticUtils.ts +++ b/src/backend/utils/SchemaDocsStaticUtils.ts @@ -10,7 +10,7 @@ import { koitoClientConfigSchema } from "../common/infrastructure/config/client/ mkdirSync(resolve(projectRootDir, 'docsite/static/schemas'), {recursive: true}); const jsonSchema = z.toJSONSchema(koitoClientConfigSchema, { - io: "input", + io: "output", //reused: "ref", unrepresentable: "any", override: (ctx) => { diff --git a/src/core/Transform.ts b/src/core/Transform.ts index 74a00748..aed7a054 100644 --- a/src/core/Transform.ts +++ b/src/core/Transform.ts @@ -106,8 +106,7 @@ export type WhenConditions = WhenParts[]; // Helper used to construct a concrete `PlayTransformPartsAtomic` schema for a given term schema, since // `PlayTransformPartsAtomic` itself can't be represented generically in zod. -function buildPartsAtomicSchema(term: T) { - return z.object({ +const buildPartsAtomicSchema = (term: T) => z.object({ title: term.optional(), artists: term.optional(), albumArtists: term.optional(), @@ -115,16 +114,27 @@ function buildPartsAtomicSchema(term: T) { duration: term.optional(), meta: term.optional(), }); -} -const whenPartsStringSchema = buildPartsAtomicSchema(z.string()); +const buildWhennablePartschema = (term: T) => z.object({ + /** A string or regex pattern matching the title of a Play */ + title: term.optional().meta({description: 'A string or regex pattern matching the title of a Play'}), + /** A string or regex pattern matching any artist of a Play */ + artists: term.optional().meta({description: 'A string or regex pattern matching any artist of a Play'}), + /** A string or regex pattern matching any album artist of a Play */ + albumArtists: term.optional().meta({description: 'A string or regex pattern matching any artist of a Play'}), + /** A string or regex pattern matching the album of a Play */ + album: term.optional().meta({description: 'A string or regex pattern matching the album of a Play'}), + }); + +const whenPartsStringSchema = buildWhennablePartschema(z.string()); export const whenConditionsConfigSchema = z.array(whenPartsStringSchema); export type WhenConditionsConfig = z.infer; export const whennableSchema = z.object({ - when: whenConditionsConfigSchema.optional() + /** Only use this Stage (or Rules) if the properties in this object match the corresponding property in a Play. The value of each property is a string or regex pattern to match. */ + when: whenConditionsConfigSchema.optional().meta({description: 'Only use this Stage (or Rules) if the properties in this object match the corresponding property in a Play. The value of each property is a string or regex pattern to match.'}) }); export type Whennable = z.infer; @@ -152,19 +162,19 @@ const { test, ...restConditionalRegSchema } = conditionalSearchAndReplaceRegExpSchema.shape; -export const conditionalSearchAndReplaceTermSchema = z.object(restConditionalRegSchema); +export const conditionalSearchAndReplaceTermSchema = z.object(restConditionalRegSchema).meta({title: 'Conditional Search and Replace Term'}); export type ConditionalSearchAndReplaceTerm = z.infer; -export const searchAndReplaceTermSchema = z.union([z.string(), conditionalSearchAndReplaceTermSchema]); +export const searchAndReplaceTermSchema = z.union([z.string(), conditionalSearchAndReplaceTermSchema]).meta({title: 'Search and Replace Term'}); export type SearchAndReplaceTerm = z.infer; -export const externalMetadataTermSchema = z.union([z.boolean(), z.undefined(), whennableSchema]); +export const externalMetadataTermSchema = z.union([z.boolean(), z.undefined(), whennableSchema]).meta({title: 'Ext Metadata Term'}); export type ExternalMetadataTerm = z.infer; -export const flowControlTermSchema = z.enum(['continue', 'stop']); +export const flowControlTermSchema = z.enum(['continue', 'stop']).meta({title: 'Flow Control Term'}); export type FlowControlTerm = z.infer; export const FLOW_CONTROL_TERM = { @@ -177,27 +187,27 @@ export const flowControlSchema = z.object({ onFailure: flowControlTermSchema, onSkip: flowControlTermSchema, failureReturnPartial: z.boolean(), -}); +}).meta({title: 'Flow Control'}); export type FlowControl = z.infer; -export const stageTypeMetadataSchema = z.enum(['spotify', 'musicbrainz', 'native']); +export const stageTypeMetadataSchema = z.enum(['spotify', 'musicbrainz', 'native']).meta({title: 'Stage Type Metadata'}); export type StageTypeMetadata = z.infer; -export const stageTypeUserSchema = z.literal('user'); +export const stageTypeUserSchema = z.literal('user').meta({title: 'Stage Type User'}); export type StageTypeUser = z.infer; // `StageTypeMetadata | StageTypeUser | string` collapses to `string` (the literal members are absorbed by // the wider `string` member), so the schema is just `z.string()`. -export const stageTypeSchema = z.string(); +export const stageTypeSchema = z.string().meta({title: 'Stage Type'}); export type StageType = z.infer; export const stageTypedConfigSchema = z.object({ type: stageTypeSchema, -}); +}).meta({title: 'Stage Type Config'}); export type StageTypedConfig = z.infer; @@ -207,7 +217,7 @@ export const stageConfigSchema = z.object({ ...flowControlSchema.partial().shape, name: z.string().optional(), stageHash: z.string().optional(), -}); +}).meta({title: 'Stage Config'}); export type StageConfig = z.infer; @@ -226,7 +236,7 @@ export const playTransformMetadataStageSchema = z.object({ ...metadataAtomicSchema.shape, score: z.number().optional(), type: stageTypeMetadataSchema, -}); +}).meta({title: 'Transform Metadata Stage'}); export type PlayTransformMetadataStage = z.infer; @@ -234,7 +244,7 @@ export const playTransformNativeStageSchema = z.object({ ...stageConfigSchema.shape, ...metadataAtomicSchema.shape, type: z.literal('native'), -}); +}).meta({title: 'Transform Native Stage'}); export type PlayTransformNativeStage = z.infer; @@ -244,7 +254,7 @@ const playTransformGenericStageSchema = z.object({ ...stageConfigSchema.shape, ...anyAtomicSchema.shape, type: stageTypeSchema, -}); +}).meta({title: 'Transform Generic Stage'}); // `PlayTransformOptions` term shape: T = SearchAndReplaceTerm[] | ExternalMetadataTerm const optionsPartsTermSchema = z.union([z.array(searchAndReplaceTermSchema), externalMetadataTermSchema]); @@ -254,22 +264,22 @@ const playTransformUserStageOptionsSchema = z.object({ ...stageConfigSchema.shape, ...optionsAtomicSchema.shape, type: stageTypeUserSchema, -}); +}).meta({title: 'Tranform User Stage'}); -const untypedPlayTransformUserStageOptionsSchema = z.object({ - ...untypedStageConfigSchema.shape, - ...optionsAtomicSchema.shape, -}); +// const untypedPlayTransformUserStageOptionsSchema = z.object({ +// ...untypedStageConfigSchema.shape, +// ...optionsAtomicSchema.shape, +// }).meta({title: 'Transform User Stage'}); // `PlayTransformRules` term shape: T = ConditionalSearchAndReplaceRegExp[] | ExternalMetadataTerm -const rulesPartsTermSchema = z.union([z.array(conditionalSearchAndReplaceRegExpSchema), externalMetadataTermSchema]); -const rulesAtomicSchema = buildPartsAtomicSchema(rulesPartsTermSchema); +const rulesPartsTermSchema = z.union([z.array(conditionalSearchAndReplaceRegExpSchema), externalMetadataTermSchema]).meta({title: 'Rule Parts Term'}); +const rulesAtomicSchema = buildPartsAtomicSchema(rulesPartsTermSchema).meta({title: 'Rules Atomic'}); const playTransformUserStageRulesSchema = z.object({ ...stageConfigSchema.shape, ...rulesAtomicSchema.shape, type: stageTypeUserSchema, -}); +}).meta({title: 'User Stage Rules'}); // zod's `discriminatedUnion` requires each branch's discriminant literal(s) to be unique across the whole // union. `StageTypeMetadata` nominally includes `'native'`, but `'native'`-typed stages are represented by @@ -278,7 +288,7 @@ const playTransformUserStageRulesSchema = z.object({ // `type` values covered across the whole union is unchanged. const metadataStageForUnionSchema = playTransformMetadataStageSchema.extend({ type: z.enum(['spotify', 'musicbrainz']), -}); +}).meta({title: 'Metadata Types'}); // `PlayTransformParts = Extract, Y> & Whennable` - the `& Whennable` intersection // is redundant here since every stage schema already includes `when` via `stageConfigSchema`/`untypedStageConfigSchema`. @@ -289,12 +299,12 @@ const playTransformTypedStageOptionsSchema = z.discriminatedUnion('type', [ metadataStageForUnionSchema, playTransformNativeStageSchema, playTransformUserStageOptionsSchema, -]); -const playTransformStageOptionsSchema = z.union([ - playTransformTypedStageOptionsSchema, - playTransformGenericStageSchema, - untypedPlayTransformUserStageOptionsSchema, -]); +]).meta({title: 'Stage'}); +// const playTransformStageOptionsSchema = z.union([ +// playTransformTypedStageOptionsSchema, +// playTransformGenericStageSchema, +// untypedPlayTransformUserStageOptionsSchema, +// ]).meta({title: 'Stage'}); // Rules pool: `Extract, StageTyped>` excludes `UntypedPlayTransformUserStage`, // since it has no `type` field and so isn't assignable to `StageTyped`. @@ -309,8 +319,8 @@ const playTransformStageRulesSchema = z.union([ ]); const playTransformPartsConfigOptionsSchema = z.union([ - z.array(playTransformStageOptionsSchema), - playTransformStageOptionsSchema, + z.array(playTransformTypedStageOptionsSchema), + playTransformTypedStageOptionsSchema, ]); export const playTransformConfigSchema = z.object({ @@ -320,7 +330,7 @@ export const playTransformConfigSchema = z.object({ existing: playTransformPartsConfigOptionsSchema.optional(), }).optional(), postCompare: playTransformPartsConfigOptionsSchema.optional(), -}); +}).meta({title: 'Transform Config'}); export type PlayTransformConfig = z.infer; @@ -328,11 +338,11 @@ export type PlayTransformConfig = z.infer; export const playTransformOptionsSchema = z.object({ ...playTransformConfigSchema.shape, log: z.union([z.boolean(), z.literal('all')]).optional(), -}); +}).meta({title: 'Transform Options'}); export type PlayTransformOptions = z.infer; -const playTransformPartsArrayRulesSchema = z.array(playTransformStageRulesSchema); +const playTransformPartsArrayRulesSchema = z.array(playTransformStageRulesSchema).meta({title: 'Transform Parts'}); /** Represents the final, strongly-typed transform configuration used during runtime. */ export const playTransformRulesSchema = z.object({ @@ -342,7 +352,7 @@ export const playTransformRulesSchema = z.object({ existing: playTransformPartsArrayRulesSchema.optional(), }).optional(), postCompare: playTransformPartsArrayRulesSchema.optional(), -}); +}).meta({title: 'Transform Rules'}); export type PlayTransformRules = z.infer; @@ -353,4 +363,4 @@ export type PlayTransformRules = z.infer; // `AbstractComponent.transformPartToStrong` and is out of scope here, so this transform is stubbed only. export const playTransformOptionsToRulesSchema = playTransformOptionsSchema.transform((val): PlayTransformRules => { throw new Error('Not implemented: use AbstractComponent.transformPartToStrong for PlayTransformOptions -> PlayTransformRules normalization'); -}); +}).meta({title: 'Transform Options to Rules'});