import * as z from "zod"; import type {SearchAndReplaceRegExp} from "@foxxmd/regex-buddy-core"; import type { MarkRequired } from "ts-essentials"; import { stripIndents } from "common-tags"; export type PlayTransformParts = Extract, Y> & Whennable; export type PlayTransformPartsArray = PlayTransformParts[]; /** Represents the weakly-defined user config. May be an array of parts or one parts object */ export type PlayTransformPartsConfig = PlayTransformPartsArray | PlayTransformParts; export interface PlayTransformPartsAtomic { title?: T artists?: T albumArtists?: T album?: T duration?: T meta?: T art?: T } export const STAGE_TYPES_USER: StageTypeUser[] = ['user']; export const STAGE_TYPES_METADATA: StageTypeMetadata[] = ['musicbrainz','native','rocksky']; export const STAGE_TYPES: StageType[] = [...STAGE_TYPES_METADATA, ...STAGE_TYPES_USER]; export interface StageTyped { type: StageType } // export interface NotStageTyped { // //type?: never // } // export type MaybeStageTyped = StageTyped | NotStageTyped; export interface AtomicStageConfig extends StageConfig, PlayTransformPartsAtomic {} export interface PlayTransformStageTyped extends PlayTransformPartsAtomic { type: StageType } export interface PlayTransformUserStage extends StageConfig, PlayTransformPartsAtomic { type: StageTypeUser } // export interface PlayTransformGenericStage extends StageConfig, PlayTransformPartsAtomic { // type: string // } // export interface UntypedPlayTransformUserStage extends UntypedStageConfig, PlayTransformPartsAtomic { // } export type PlayTransformStage = PlayTransformMetadataStage | PlayTransformUserStage | PlayTransformNativeStage;// | UntypedPlayTransformUserStage | PlayTransformGenericStage; /** Represents the plain json user-configured structure (input) */ export interface PlayTransformHooksConfig { preCompare?: PlayTransformPartsConfig compare?: { candidate?: PlayTransformPartsConfig existing?: PlayTransformPartsConfig } postCompare?: PlayTransformPartsConfig } /** Represents the final, strongly-typed transform configuration used during runtime */ export interface PlayTransformHooks extends PlayTransformHooksConfig { preCompare?: PlayTransformPartsArray compare?: { candidate?: PlayTransformPartsArray existing?: PlayTransformPartsArray } postCompare?: PlayTransformPartsArray } export type TransformHook = 'preCompare' | 'compare' | 'candidate' | 'existing' | 'postCompare'; export const TRANSFORM_HOOK = { preCompare: 'preCompare' as TransformHook, candidate: 'candidate' as TransformHook, existing: 'existing' as TransformHook, postCompare: 'postCompare' as TransformHook, } export type WhenParts = PlayTransformPartsAtomic; 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. const buildPartsAtomicSchema = (term: T) => z.object({ title: term.optional(), artists: term.optional(), albumArtists: term.optional(), album: term.optional(), duration: term.optional(), meta: term.optional(), art: term.optional() }); 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'}), /** A string or regex pattern matching any of the art links for a Play */ art: term.optional().meta({description: 'A string or regex pattern matching any of the art links for a Play'}), }); const whenPartsStringSchema = buildWhennablePartschema(z.string()); export const whenConditionsConfigSchema = z.array(whenPartsStringSchema); export type WhenConditionsConfig = z.infer; export const whennableSchema = z.object({ /** 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; export const conditionalSearchAndReplaceRegExpSchema = z.object({ ...whennableSchema.shape, /** The plain string or regex pattern to match */ search: z.string().meta({description: 'The plain string or regex pattern to match'}), /** The replacement string. Can be empty string or use js replacement string pattern */ replace: z.string().meta({description: 'The replacement string. Can be empty string or use js replacement string pattern'}), test: z.custom<(obj: SearchAndReplaceRegExp) => boolean>((val) => typeof val === 'function').optional(), }); // need to set tsconfig compiler option "strictNullChecks": true to make the inferred type not have search as optional // but it causes too many errors at the moment // so workaround by explicitly marking it as required for this type // https://stackoverflow.com/a/77256318 export type ConditionalSearchAndReplaceRegExp = MarkRequired, 'search'>; const { test, ...restConditionalRegSchema } = conditionalSearchAndReplaceRegExpSchema.shape; 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]).meta({title: 'Search and Replace Term'}); export type SearchAndReplaceTerm = z.infer; export const externalMetadataTermSchema = z.union([z.boolean(), whennableSchema]).optional().meta({title: 'Ext Metadata Term'}); export type ExternalMetadataTerm = z.infer; export const flowControlTermSchema = z.enum(['continue', 'stop']).meta({title: 'Flow Control Term'}); export type FlowControlTerm = z.infer; export const FLOW_CONTROL_TERM = { continue: 'continue', stop: 'stop' } as const satisfies Record; export const flowControlSchema = z.object({ onSuccess: flowControlTermSchema, onFailure: flowControlTermSchema, onSkip: flowControlTermSchema, failureReturnPartial: z.boolean(), }).meta({title: 'Flow Control'}); export type FlowControl = z.infer; export const stageTypeMetadataSchema = z.enum(['musicbrainz', 'native', 'rocksky']).meta({title: 'Stage Type Metadata'}); export const typedStageSchema = z.enum(['musicbrainz', 'native','user', 'rocksky']).meta({title: 'Stage Type'}); export type StageTypeMetadata = z.infer; export const stageTypeUserSchema = z.literal('user').meta({title: 'Stage Type User'}); export type StageTypeUser = z.infer; export const stageTypeSchema = z.string().meta({title: 'Stage Type'}); export type StageType = z.infer; export const stageTypedConfigSchema = z.object({ type: typedStageSchema, }).meta({title: 'Stage Type Config'}); export type StageTypedConfig = z.infer; export const stageConfigSchema = z.object({ ...stageTypedConfigSchema.shape, ...whennableSchema.shape, ...flowControlSchema.partial().shape, name: z.string().optional(), stageHash: z.string().optional(), }).meta({title: 'Stage Config'}); export type StageConfig = z.infer; export const untypedStageConfigSchema = z.object({ ...whennableSchema.shape, ...flowControlSchema.partial().shape, name: z.string().optional(), }); export type UntypedStageConfig = z.infer; const metadataAtomicSchema = buildPartsAtomicSchema(externalMetadataTermSchema); export const playTransformMetadataStageSchema = z.object({ ...stageConfigSchema.shape, ...metadataAtomicSchema.shape, score: z.number().optional(), type: stageTypeMetadataSchema, }).meta({title: 'Transform Metadata Stage'}); export type PlayTransformMetadataStage = z.infer; export const playTransformNativeStageSchema = z.object({ ...stageConfigSchema.shape, ...metadataAtomicSchema.shape, type: z.literal('native'), }).meta({title: 'Transform Native Stage'}); export type PlayTransformNativeStage = z.infer; //const anyAtomicSchema = buildPartsAtomicSchema(z.any()); // const playTransformGenericStageSchema = z.object({ // ...stageConfigSchema.shape, // ...anyAtomicSchema.shape, // type: stageTypeSchema, // }).meta({title: 'Transform Generic Stage'}); const optionsAtomicSchema = buildPartsAtomicSchema(z.array(searchAndReplaceTermSchema)); const playTransformUserStageOptionsSchema = z.object({ ...stageConfigSchema.shape, ...optionsAtomicSchema.shape, type: z.literal('user'), }).meta({title: 'Tranform User Stage'}); // const untypedPlayTransformUserStageOptionsSchema = z.object({ // ...untypedStageConfigSchema.shape, // ...optionsAtomicSchema.shape, // }).meta({title: 'Transform User Stage'}); const rulesAtomicSchema = buildPartsAtomicSchema(z.array(searchAndReplaceTermSchema)).meta({title: 'Rules Atomic'}); const playTransformUserStageRulesSchema = z.object({ ...stageConfigSchema.shape, ...rulesAtomicSchema.shape, type: z.literal('user'), }).meta({title: 'User Stage Rules'}); const metadataStageForUnionSchema = playTransformMetadataStageSchema.extend({ type: z.enum(['musicbrainz','rocksky']), }).meta({title: 'Transform External Stage'}); const playTransformTypedStageOptionsSchema = z.discriminatedUnion('type', [ metadataStageForUnionSchema, playTransformNativeStageSchema, playTransformUserStageOptionsSchema, ]).meta({title: 'Stage'}); // const playTransformStageOptionsSchema = z.union([ // playTransformTypedStageOptionsSchema, // playTransformGenericStageSchema, // untypedPlayTransformUserStageOptionsSchema, // ]).meta({title: 'Stage'}); const playTransformTypedStageRulesSchema = z.discriminatedUnion('type', [ metadataStageForUnionSchema, playTransformNativeStageSchema, playTransformUserStageRulesSchema, ]); const playTransformPartsConfigOptionsSchema = z.union([ z.array(playTransformTypedStageOptionsSchema), playTransformTypedStageOptionsSchema, ]); export const playTransformConfigSchema = z.object({ /** Stages to be applied when a Play is first seen by this component */ preCompare: playTransformPartsConfigOptionsSchema.optional().meta({description: 'Stages to be applied when a Play is first seen by this component'}), /** Stages to be applied when comparing a candidate Play to any existing Plays such as when checking for duplicates or discovered Plays * * **Note:** Transforms applies are not persistent. They are used only during comparison operations. */ compare: z.object({ /** Stages to apply to the Play being considered for discovery or scrobbling */ candidate: playTransformPartsConfigOptionsSchema.optional().meta({description: `Stages to apply to the Play being considered for discovery or scrobbling`}), /** Stages to apply to the existing Plays (already discovered, existing scrobbles) */ existing: playTransformPartsConfigOptionsSchema.optional().meta({description: `Stages to apply to the existing Plays (already discovered, existing scrobbles)`}), }).optional().meta({description: stripIndents`Stages to be applied when comparing a candidate Play to any existing Plays such as when checking for duplicates or discovered Plays **Note:** Transforms applies are not persistent. They are used only during comparison operations.`}), postCompare: playTransformPartsConfigOptionsSchema.optional().meta({ description: 'Stages applied before Play is sent downstream (to MS Client or just before being scrobbling)' }), }).meta({title: 'Transform Config'}); export type PlayTransformConfig = z.infer; /** Represents the plain json user-configured structure (input). Used to validate user input (json). */ export const playTransformOptionsSchema = z.object({ ...playTransformConfigSchema.shape, log: z.union([z.boolean(), z.literal('all')]).optional(), }).meta({title: 'Transform Options', description: 'Enhance/correct Play data by applying a transform pipeline'}); export type PlayTransformOptions = z.infer; const playTransformPartsArrayRulesSchema = z.array(playTransformTypedStageRulesSchema).meta({title: 'Transform Parts'}); /** Represents the final, strongly-typed transform configuration used during runtime. */ export const playTransformRulesSchema = z.object({ preCompare: playTransformPartsArrayRulesSchema.optional().meta({description: 'Stages to be applied when a Play is first seen by this component'}), compare: z.object({ candidate: playTransformPartsArrayRulesSchema.optional(), existing: playTransformPartsArrayRulesSchema.optional(), }).optional(), postCompare: playTransformPartsArrayRulesSchema.optional(), }).meta({title: 'Transform Rules'}); export type PlayTransformRules = z.infer; 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'});