/** * @since 1.0.0 */ import type { Brand } from "effect/Brand" import type * as Effect from "effect/Effect" import { constUndefined, dual } from "effect/Function" import * as Option from "effect/Option" import * as ParseResult from "effect/ParseResult" import { type Pipeable, pipeArguments } from "effect/Pipeable" import * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import type * as AST from "effect/SchemaAST" import * as Struct_ from "effect/Struct" /** * @since 1.0.0 * @category type ids */ export const TypeId: unique symbol = Symbol.for("@effect/experimental/VariantSchema") /** * @since 1.0.0 * @category type ids */ export type TypeId = typeof TypeId const cacheSymbol = Symbol.for("@effect/experimental/VariantSchema/cache") /** * @since 1.0.0 * @category models */ export interface Struct extends Pipeable { readonly [TypeId]: A /** @internal */ [cacheSymbol]?: Record } /** * @since 1.0.0 * @category guards */ export const isStruct = (u: unknown): u is Struct => Predicate.hasProperty(u, TypeId) /** * @since 1.0.0 * @category models */ export declare namespace Struct { /** * @since 1.0.0 * @category models */ export type Any = { readonly [TypeId]: any } /** * @since 1.0.0 * @category models */ export type Fields = { readonly [key: string]: | Schema.Schema.All | Schema.PropertySignature.All | Field | Struct | undefined } /** * @since 1.0.0 * @category models */ export type Validate = { readonly [K in keyof A]: A[K] extends { readonly [TypeId]: infer _ } ? Validate : A[K] extends Field ? [keyof Config] extends [Variant] ? {} : "field must have valid variants" : {} } } /** * @since 1.0.0 * @category type ids */ export const FieldTypeId: unique symbol = Symbol.for( "@effect/experimental/VariantSchema/Field" ) /** * @since 1.0.0 * @category type ids */ export type FieldTypeId = typeof FieldTypeId /** * @since 1.0.0 * @category models */ export interface Field extends Pipeable { readonly [FieldTypeId]: FieldTypeId readonly schemas: A } /** * @since 1.0.0 * @category guards */ export const isField = (u: unknown): u is Field => Predicate.hasProperty(u, FieldTypeId) /** * @since 1.0.0 * @category models */ export declare namespace Field { /** * @since 1.0.0 * @category models */ export type Any = { readonly [FieldTypeId]: FieldTypeId } /** * @since 1.0.0 * @category models */ type ValueAny = Schema.Schema.All | Schema.PropertySignature.All /** * @since 1.0.0 * @category models */ export type Config = { readonly [key: string]: Schema.Schema.All | Schema.PropertySignature.All | undefined } /** * @since 1.0.0 * @category models */ export type ConfigWithKeys = { readonly [P in K]?: Schema.Schema.All | Schema.PropertySignature.All } /** * @since 1.0.0 * @category models */ export type Fields = { readonly [key: string]: | Schema.Schema.All | Schema.PropertySignature.All | Field | Struct | undefined } } /** * @since 1.0.0 * @category extractors */ export type ExtractFields = { readonly [ K in keyof Fields as [Fields[K]] extends [Field] ? V extends keyof Config ? K : never : K ]: [Fields[K]] extends [Struct] ? Extract : [Fields[K]] extends [Field] ? [Config[V]] extends [Schema.Schema.All | Schema.PropertySignature.All] ? Config[V] : never : [Fields[K]] extends [Schema.Schema.All | Schema.PropertySignature.All] ? Fields[K] : never } /** * @since 1.0.0 * @category extractors */ export type Extract, IsDefault = false> = [A] extends [ Struct ] ? IsDefault extends true ? [A] extends [Schema.Schema.Any] ? A : Schema.Struct>> : Schema.Struct>> : never const extract: { ( variant: V, options?: { readonly isDefault?: IsDefault | undefined } ): >(self: A) => Extract , const IsDefault extends boolean = false>(self: A, variant: V, options?: { readonly isDefault?: IsDefault | undefined }): Extract } = dual( (args) => isStruct(args[0]), >( self: A, variant: V, options?: { readonly isDefault?: boolean | undefined } ): Extract => { const cache = self[cacheSymbol] ?? (self[cacheSymbol] = {}) const cacheKey = options?.isDefault === true ? "__default" : variant if (cache[cacheKey] !== undefined) { return cache[cacheKey] as any } const fields: Record = {} for (const key of Object.keys(self[TypeId])) { const value = self[TypeId][key] if (TypeId in value) { if (options?.isDefault === true && Schema.isSchema(value)) { fields[key] = value } else { fields[key] = extract(value, variant) } } else if (FieldTypeId in value) { if (variant in value.schemas) { fields[key] = value.schemas[variant] } } else { fields[key] = value } } return cache[cacheKey] = Schema.Struct(fields) as any } ) /** * @category accessors * @since 1.0.0 */ export const fields = >(self: A): A[TypeId] => self[TypeId] type RequiredKeys = { [K in keyof T]-?: {} extends Pick ? never : K }[keyof T] /** * @since 1.0.0 * @category models */ export interface Class< Self, Fields extends Struct.Fields, SchemaFields extends Schema.Struct.Fields, A, I, R, C > extends Schema.Schema, R>, Struct> { new( props: RequiredKeys extends never ? void | Schema.Simplify : Schema.Simplify, options?: { readonly disableValidation?: boolean } ): A readonly ast: AST.Transformation make, X>( this: { new(...args: Args): X }, ...args: Args ): X annotations( annotations: Schema.Annotations.Schema ): Schema.SchemaClass readonly identifier: string readonly fields: Schema.Simplify } type ClassFromFields< Self, Fields extends Struct.Fields, SchemaFields extends Schema.Struct.Fields > = Class< Self, Fields, SchemaFields, Schema.Struct.Type, Schema.Struct.Encoded, Schema.Struct.Context, Schema.Struct.Constructor > type MissingSelfGeneric = `Missing \`Self\` generic - use \`class Self extends Class()(${Params}{ ... })\`` /** * @since 1.0.0 * @category models */ export interface Union>> extends Schema.Union< { readonly [K in keyof Members]: [Members[K]] extends [Schema.Schema.All] ? Members[K] : never } > {} /** * @since 1.0.0 * @category models */ export declare namespace Union { /** * @since 1.0.0 * @category models */ export type Variants>, Variants extends string> = { readonly [Variant in Variants]: Schema.Union< { [K in keyof Members]: Extract } > } } /** * @since 1.0.0 * @category models */ export interface fromKey extends Schema.PropertySignature< ":", Schema.Schema.Type, Key, ":", Schema.Schema.Encoded, false, Schema.Schema.Context > {} /** * @since 1.0.0 * @category models */ export declare namespace fromKey { /** * @since 1.0.0 */ export type Rename = S extends Schema.PropertySignature< infer _TypeToken, infer _Type, infer _Key, infer _EncodedToken, infer _Encoded, infer _HasDefault, infer _R > ? Schema.PropertySignature<_TypeToken, _Type, Key, _EncodedToken, _Encoded, _HasDefault, _R> : S extends Schema.Schema.All ? fromKey : never } /** * @since 1.0.0 * @category constructors */ export const make = < const Variants extends ReadonlyArray, const Default extends Variants[number] >(options: { readonly variants: Variants readonly defaultVariant: Default }): { readonly Struct: ( fields: A & Struct.Validate ) => Struct readonly Field: >( config: A & { readonly [K in Exclude]: never } ) => Field readonly FieldOnly: >( ...keys: Keys ) => ( schema: S ) => Field<{ readonly [K in Keys[number]]: S }> readonly FieldExcept: >( ...keys: Keys ) => ( schema: S ) => Field<{ readonly [K in Exclude]: S }> readonly fieldEvolve: { < Self extends Field | Field.ValueAny, const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny } : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) >(f: Mapping): (self: Self) => Field< Self extends Field ? { readonly [K in keyof S]: K extends keyof Mapping ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] : S[K] } : { readonly [K in Variants[number]]: K extends keyof Mapping ? Mapping[K] extends (arg: any) => any ? ReturnType : Self : Self } > < Self extends Field | Field.ValueAny, const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: (variant: S[K]) => Field.ValueAny } : { readonly [K in Variants[number]]?: (variant: Self) => Field.ValueAny }) >(self: Self, f: Mapping): Field< Self extends Field ? { readonly [K in keyof S]: K extends keyof Mapping ? Mapping[K] extends (arg: any) => any ? ReturnType : S[K] : S[K] } : { readonly [K in Variants[number]]: K extends keyof Mapping ? Mapping[K] extends (arg: any) => any ? ReturnType : Self : Self } > } readonly fieldFromKey: { < Self extends Field | Field.ValueAny, const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: string } : { readonly [K in Variants[number]]?: string }) >( mapping: Mapping ): (self: Self) => Field< Self extends Field ? { readonly [K in keyof S]: K extends keyof Mapping ? Mapping[K] extends string ? fromKey.Rename : S[K] : S[K] } : { readonly [K in Variants[number]]: K extends keyof Mapping ? Mapping[K] extends string ? fromKey.Rename : Self : Self } > < Self extends Field | Field.ValueAny, const Mapping extends (Self extends Field ? { readonly [K in keyof S]?: string } : { readonly [K in Variants[number]]?: string }) >( self: Self, mapping: Mapping ): Field< Self extends Field ? { readonly [K in keyof S]: K extends keyof Mapping ? Mapping[K] extends string ? fromKey.Rename : S[K] : S[K] } : { readonly [K in Variants[number]]: K extends keyof Mapping ? Mapping[K] extends string ? fromKey.Rename : Self : Self } > } readonly Class: ( identifier: string ) => ( fields: Fields & Struct.Validate, annotations?: Schema.Annotations.Schema ) => [Self] extends [never] ? MissingSelfGeneric : & ClassFromFields< Self, Fields, ExtractFields > & { readonly [V in Variants[number]]: Extract> } readonly Union: >>( ...members: Members ) => Union & Union.Variants readonly extract: { ( variant: V ): >(self: A) => Extract >( self: A, variant: V ): Extract } } => { function Class(identifier: string) { return function( fields: Struct.Fields, annotations?: Schema.Annotations.Schema ) { const variantStruct = Struct(fields) const schema = extract(variantStruct, options.defaultVariant, { isDefault: true }) class Base extends Schema.Class(identifier)(schema.fields, annotations) { static [TypeId] = fields } for (const variant of options.variants) { Object.defineProperty(Base, variant, { value: extract(variantStruct, variant).annotations({ identifier: `${identifier}.${variant}`, title: `${identifier}.${variant}` }) }) } return Base } } function FieldOnly(...keys: Keys) { return function(schema: S) { const obj: Record = {} for (const key of keys) { obj[key] = schema } return Field(obj) } } function FieldExcept(...keys: Keys) { return function(schema: S) { const obj: Record = {} for (const variant of options.variants) { if (!keys.includes(variant)) { obj[variant] = schema } } return Field(obj) } } function UnionVariants(...members: ReadonlyArray>) { return Union(members, options.variants) } const fieldEvolve = dual( 2, ( self: Field | Schema.Schema.All | Schema.PropertySignature.All, f: Record Field.ValueAny> ): Field => { const field = isField(self) ? self : Field(Object.fromEntries( options.variants.map((variant) => [variant, self]) )) return Field(Struct_.evolve(field.schemas, f)) } ) const fieldFromKey = dual( 2, ( self: | Field<{ readonly [key: string]: Schema.Schema.All | Schema.PropertySignature.Any | undefined }> | Schema.Schema.All | Schema.PropertySignature.Any, mapping: Record ): Field => { const obj: Record = {} if (isField(self)) { for (const [key, schema] of Object.entries(self.schemas)) { obj[key] = mapping[key] !== undefined ? renameFieldValue(schema as any, mapping[key]) : schema } } else { for (const key of options.variants) { obj[key] = mapping[key] !== undefined ? renameFieldValue(self as any, mapping[key]) : self } } return Field(obj) } ) const extractVariants = dual( 2, (self: Struct, variant: string): any => extract(self, variant, { isDefault: variant === options.defaultVariant }) ) return { Struct, Field, FieldOnly, FieldExcept, Class, Union: UnionVariants, fieldEvolve, fieldFromKey, extract: extractVariants } as any } /** * @since 1.0.0 * @category overrideable */ export const Override = (value: A): A & Brand<"Override"> => value as any /** * @since 1.0.0 * @category overrideable */ export interface Overrideable extends Schema.PropertySignature<":", (To & Brand<"Override">) | undefined, never, ":", From, true, R> {} /** * @since 1.0.0 * @category overrideable */ export const Overrideable = ( from: Schema.Schema, to: Schema.Schema, options: { readonly generate: (_: Option.Option) => Effect.Effect readonly decode?: Schema.Schema readonly constructorDefault?: () => To } ): Overrideable => Schema.transformOrFail( from, Schema.Union(Schema.Undefined, to as Schema.brand, "Override">), { decode: (_) => options.decode ? ParseResult.decode(options.decode)(_) : ParseResult.succeed(undefined), encode: (dt) => options.generate(dt === undefined ? Option.none() : Option.some(dt)) } ).pipe(Schema.propertySignature, Schema.withConstructorDefault(options.constructorDefault ?? constUndefined as any)) const StructProto = { pipe() { return pipeArguments(this, arguments) } } const Struct = (fields: A): Struct => { const self = Object.create(StructProto) self[TypeId] = fields return self } const FieldProto = { [FieldTypeId]: FieldTypeId, pipe() { return pipeArguments(this, arguments) } } const Field = (schemas: A): Field => { const self = Object.create(FieldProto) self.schemas = schemas return self } const Union = >, Variants extends ReadonlyArray>( members: Members, variants: Variants ) => { class VariantUnion extends (Schema.Union(...members.filter((member) => Schema.isSchema(member))) as any) {} for (const variant of variants) { Object.defineProperty(VariantUnion, variant, { value: Schema.Union(...members.map((member) => extract(member, variant))) }) } return VariantUnion } const renameFieldValue = ( self: F, key: string ) => Schema.isPropertySignature(self) ? Schema.fromKey(self, key) : Schema.fromKey(Schema.propertySignature(self), key)