/* eslint-disable @typescript-eslint/no-explicit-any */ import type * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import * as S from "effect/Schema" import * as SchemaAST from "effect/SchemaAST" import * as SchemaIssue from "effect/SchemaIssue" import { copyOrigin } from "../utils.js" import { concurrencyUnbounded } from "./ext.js" import * as SchemaParser from "./SchemaParser.js" type ClassAnnotations = S.Annotations.Declaration export interface EnhancedClass extends S.Class { /** * See `copyOrigin` docs in `utils.ts` for return-type design details. */ readonly copy: ReturnType Self>> } type MissingSelfGeneric = `Missing \`Self\` generic - use \`class Self extends ${Usage}()(${Params}{ ... })\`` type HasFields = { readonly fields: Fields } | { readonly from: HasFields } type ClassOptions = { readonly strict?: boolean } export declare const ExtendedSchemaNoEncoded: unique symbol export type ExtendedSchemaNoEncoded = typeof ExtendedSchemaNoEncoded type WithEncoded = Omit & { readonly Encoded: Encoded } type ExtendedSchema = [Encoded] extends [ExtendedSchemaNoEncoded] ? SchemaS : WithEncoded export type Class = EnhancedClass< Self, S, Inherited > /** * Build a modified Declaration that accepts struct-matching values during * encoding, given the original Declaration and the class's fields. */ function makeRelaxedDeclaration( ast: SchemaAST.Declaration, fields: S.Struct.Fields, cls: any ): SchemaAST.Declaration { const parseOptions = ast.annotations?.["parseOptions"] as SchemaAST.ParseOptions | undefined const structSchema = S.Struct(fields) const annotatedStruct = parseOptions ? S.toType(structSchema).annotate({ parseOptions }) : S.toType(structSchema) const decodeStruct = SchemaParser.decodeUnknownEffect(annotatedStruct) return new SchemaAST.Declaration( ast.typeParameters, () => (input: unknown, self: SchemaAST.Declaration, options: SchemaAST.ParseOptions) => { if (input instanceof cls) { return Effect.succeed(input) } if (input !== null && typeof input === "object") { return decodeStruct(input, options) } return Effect.fail(new SchemaIssue.InvalidType(self, Option.some(input))) }, ast.annotations, ast.checks, ast.encoding, ast.context ) } // --------------------------------------------------------------------------- // Class — like Schema.Class but with relaxed encoding // --------------------------------------------------------------------------- /** * Like `Schema.Class`, but the resulting class accepts plain objects matching * the struct schema during encoding — not only `instanceof` or type-id * checks. * * @example * ```ts * import * as Schema from "effect/Schema" * import { Class } from "./Class.js" * * class A extends Class("A")({ a: Schema.String }) {} * * // Construction works as normal: * new A({ a: "hello" }) * * // Encoding accepts plain objects: * Schema.encodeUnknownSync(A)({ a: "hello" }) // { a: "hello" } * ``` */ export const Class: ( identifier: string ) => ( fieldsOr: Fields | HasFields, annotations?: ClassAnnotations, options?: ClassOptions ) => [Self] extends [never] ? MissingSelfGeneric<"Class"> : EnhancedClass< Self, ExtendedSchema, Encoded>, Brand > = (identifier) => (fields, annotations, options) => { const relaxed = options?.strict === false // Build the original Schema.Class const Base = (S.Class as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) // Get the original ast getter from the base class const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! // Cache per-class to avoid recomputing const astCache = new WeakMap() const copyCache = new WeakMap>() return class extends Base { static get copy() { let cached = copyCache.get(this) if (cached === undefined) { cached = copyOrigin(this) copyCache.set(this, cached) } return cached } static get ast(): SchemaAST.Declaration { let cached = astCache.get(this) if (cached !== undefined) return cached // Call the original getter with `this` bound to the actual user class, // so getClassSchema(this) creates a schema that uses `new this(...)`. const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst astCache.set(this, cached) return cached } static mapFields(f: any, options?: any) { return Base.mapFields(f, options).annotate(concurrencyUnbounded) } } as any } // --------------------------------------------------------------------------- // TaggedClass — like Schema.TaggedClass but with relaxed encoding // --------------------------------------------------------------------------- /** * Like `Schema.TaggedClass`, but the resulting class accepts plain objects * matching the struct schema during encoding. * * @example * ```ts * import * as Schema from "effect/Schema" * import { TaggedClass } from "./Class.js" * * class Circle extends TaggedClass()("Circle", { * radius: Schema.Number * }) {} * * Schema.encodeUnknownSync(Circle)({ _tag: "Circle", radius: 5 }) * ``` */ export const TaggedClass: ( identifier?: string ) => ( tag: Tag, fieldsOr: Fields | HasFields, annotations?: ClassAnnotations, options?: ClassOptions ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedClass"> : EnhancedClass< Self, ExtendedSchema } & Fields>, Encoded>, Brand > = (identifier) => (tag, fields, annotations, options) => { const relaxed = options?.strict === false const Base = (S.TaggedClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() return class extends Base { static get copy() { let cached = copyCache.get(this) if (cached === undefined) { cached = copyOrigin(this) copyCache.set(this, cached) } return cached } static get ast(): SchemaAST.Declaration { let cached = astCache.get(this) if (cached !== undefined) return cached const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst astCache.set(this, cached) return cached } static mapFields(f: any, options?: any) { return Base.mapFields(f, options).annotate(concurrencyUnbounded) } } as any } // --------------------------------------------------------------------------- // ErrorClass — like Schema.ErrorClass but with relaxed encoding // --------------------------------------------------------------------------- export const ErrorClass: ( identifier: string ) => ( fieldsOr: Fields | HasFields, annotations?: ClassAnnotations, options?: ClassOptions ) => [Self] extends [never] ? MissingSelfGeneric<"ErrorClass"> : EnhancedClass< Self, ExtendedSchema, Encoded>, Cause.YieldableError & Brand > = (identifier) => (fields, annotations, options) => { const relaxed = options?.strict === false const Base = (S.ErrorClass as any)(identifier)(fields, { ...concurrencyUnbounded, ...annotations }) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() return class extends Base { static get copy() { let cached = copyCache.get(this) if (cached === undefined) { cached = copyOrigin(this) copyCache.set(this, cached) } return cached } static get ast(): SchemaAST.Declaration { let cached = astCache.get(this) if (cached !== undefined) return cached const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst astCache.set(this, cached) return cached } static mapFields(f: any, options?: any) { return Base.mapFields(f, options).annotate(concurrencyUnbounded) } } as any } // --------------------------------------------------------------------------- // TaggedErrorClass — like Schema.TaggedErrorClass but with relaxed encoding // --------------------------------------------------------------------------- export const TaggedErrorClass: ( identifier?: string ) => ( tag: Tag, fieldsOr: Fields | HasFields, annotations?: ClassAnnotations, options?: ClassOptions ) => [Self] extends [never] ? MissingSelfGeneric<"TaggedErrorClass"> : EnhancedClass< Self, ExtendedSchema } & Fields>, Encoded>, Cause.YieldableError & Brand > = (identifier) => (tag, fields, annotations, options) => { const relaxed = options?.strict === false const Base = (S.TaggedErrorClass as any)(identifier)(tag, fields, { ...concurrencyUnbounded, ...annotations }) const originalAstDescriptor = Object.getOwnPropertyDescriptor(Base, "ast")! const astCache = new WeakMap() const copyCache = new WeakMap>() return class extends Base { static get copy() { let cached = copyCache.get(this) if (cached === undefined) { cached = copyOrigin(this) copyCache.set(this, cached) } return cached } static get ast(): SchemaAST.Declaration { let cached = astCache.get(this) if (cached !== undefined) return cached const originalAst = originalAstDescriptor.get!.call(this) as SchemaAST.Declaration cached = relaxed ? makeRelaxedDeclaration(originalAst, Base.fields, this) : originalAst astCache.set(this, cached) return cached } static mapFields(f: any, options?: any) { return Base.mapFields(f, options).annotate(concurrencyUnbounded) } } as any } export interface Opaque extends S.Opaque, Brand> {} export const Opaque: () => ( schema: S ) => Opaque & Omit = S.Opaque as any