1/**
2 * Builds related schemas for named variants from shared field definitions.
3 *
4 * `make` fixes the variant names and default variant, then lets callers define
5 * fields that are shared by all variants or specific to some variants. From
6 * those definitions it can create schema classes, unions, extracted struct
7 * schemas, and helpers for changing fields across variants.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import type { Brand } from "../Brand.ts"
13import * as Effect from "../Effect.ts"
14import { dual } from "../Function.ts"
15import * as InternalRecord from "../internal/record.ts"
16import { type Pipeable, pipeArguments } from "../Pipeable.ts"
17import * as Predicate from "../Predicate.ts"
18import * as Schema from "../Schema.ts"
19import * as Struct_ from "../Struct.ts"
20
21/**
22 * Runtime type identifier attached to variant schema structs.
23 *
24 * @stability unstable
25 * @category type IDs
26 * @since 4.0.0
27 */
28export const TypeId = "~effect/schema/VariantSchema"
29
30const cacheSymbol = Symbol.for(`${TypeId}/cache`)
31const defaultCacheSymbol = Symbol.for(`${TypeId}/defaultCache`)
32
33/**
34 * Pipeable container of schema fields that can be extracted into per-variant
35 * `Schema.Struct` schemas.
36 *
37 * @stability unstable
38 * @category models
39 * @since 4.0.0
40 */
41export interface Struct<in out A extends Field.Fields> extends Pipeable {
42 readonly [TypeId]: A
43 /**
44 * @internal
45 */
46 [cacheSymbol]?: Record<string, Schema.Top>
47 /**
48 * @internal
49 */
50 [defaultCacheSymbol]?: Record<string, Schema.Top>
51}
52
53/**
54 * Returns `true` when a value is a variant schema struct.
55 *
56 * @stability unstable
57 * @category guards
58 * @since 4.0.0
59 */
60export const isStruct = (u: unknown): u is Struct<any> => Predicate.hasProperty(u, TypeId)
61
62/**
63 * Type-level helpers for variant schema structs.
64 *
65 * @stability unstable
66 * @since 4.0.0
67 */
68export declare namespace Struct {
69 /**
70 * Minimal structural type for any variant schema struct.
71 *
72 * @stability unstable
73 * @category models
74 * @since 4.0.0
75 */
76 export type Any = { readonly [TypeId]: any }
77
78 /**
79 * Field map accepted by a variant struct, where each property may be a schema, a
80 * variant field, a nested struct, or `undefined`.
81 *
82 * @stability unstable
83 * @category models
84 * @since 4.0.0
85 */
86 export type Fields = {
87 readonly [key: string]:
88 | Schema.Top
89 | Field<any>
90 | Struct<any>
91 | undefined
92 }
93
94 /**
95 * Type-level validation that every variant field in a struct only uses variants
96 * from the configured variant set.
97 *
98 * @stability unstable
99 * @category models
100 * @since 4.0.0
101 */
102 export type Validate<A, Variant extends string> = {
103 readonly [K in keyof A]: A[K] extends { readonly [TypeId]: infer _ } ? Validate<A[K], Variant> :
104 A[K] extends Field<infer Config> ? [keyof Config] extends [Variant] ? {} : "field must have valid variants"
105 : unknown
106 }
107}
108
109const FieldTypeId = "~effect/schema/VariantSchema/Field"
110
111/**
112 * Pipeable collection of variant-specific schemas for a single logical field.
113 *
114 * @stability unstable
115 * @category models
116 * @since 4.0.0
117 */
118export interface Field<in out A extends Field.Config> extends Pipeable {
119 readonly [FieldTypeId]: typeof FieldTypeId
120 readonly schemas: A
121}
122
123/**
124 * Returns `true` when a value is a variant schema field.
125 *
126 * @stability unstable
127 * @category guards
128 * @since 4.0.0
129 */
130export const isField = (u: unknown): u is Field<any> => Predicate.hasProperty(u, FieldTypeId)
131
132/**
133 * Type-level helpers for variant schema fields.
134 *
135 * @stability unstable
136 * @since 4.0.0
137 */
138export declare namespace Field {
139 /**
140 * Minimal structural type for any variant schema field.
141 *
142 * @stability unstable
143 * @category models
144 * @since 4.0.0
145 */
146 export type Any = { readonly [FieldTypeId]: typeof FieldTypeId }
147
148 /**
149 * Map from variant name to the schema used for a field in that variant.
150 *
151 * @stability unstable
152 * @category models
153 * @since 4.0.0
154 */
155 export type Config = {
156 readonly [key: string]: Schema.Top | undefined
157 }
158
159 /**
160 * Variant field configuration restricted to an optional subset of the supplied
161 * variant keys.
162 *
163 * @stability unstable
164 * @category models
165 * @since 4.0.0
166 */
167 export type ConfigWithKeys<K extends string> = {
168 readonly [P in K]?: Schema.Top | undefined
169 }
170
171 /**
172 * Field map whose properties may be schemas, variant fields, nested structs, or
173 * `undefined`.
174 *
175 * @stability unstable
176 * @category models
177 * @since 4.0.0
178 */
179 export type Fields = {
180 readonly [key: string]:
181 | Schema.Top
182 | Field<any>
183 | Struct<any>
184 | undefined
185 }
186}
187
188/**
189 * Computes the `Schema.Struct` field map for a variant by selecting matching
190 * field schemas and recursively extracting nested structs.
191 *
192 * @stability unstable
193 * @category utility types
194 * @since 4.0.0
195 */
196export type ExtractFields<V extends string, Fields extends Struct.Fields, IsDefault = false> = {
197 readonly [
198 K in keyof Fields as [Fields[K]] extends [Field<infer Config>] ? V extends keyof Config ? K
199 : never
200 : K
201 ]: [Fields[K]] extends [Struct<infer _>] ? Extract<V, Fields[K], IsDefault>
202 : [Fields[K]] extends [Field<infer Config>] ? [Config[V]] extends [Schema.Top] ? Config[V]
203 : never
204 : [Fields[K]] extends [Schema.Top] ? Fields[K]
205 : never
206}
207
208/**
209 * Computes the schema type produced by extracting a single variant from a variant
210 * schema struct.
211 *
212 * @stability unstable
213 * @category utility types
214 * @since 4.0.0
215 */
216export type Extract<V extends string, A extends Struct<any>, IsDefault = false> = [A] extends [
217 Struct<infer Fields>
218] ? IsDefault extends true ? [A] extends [Schema.Top] ? A : Schema.Struct<Struct_.Simplify<ExtractFields<V, Fields>>>
219 : Schema.Struct<Struct_.Simplify<ExtractFields<V, Fields>>>
220 : never
221
222const extract: {
223 <V extends string, const IsDefault extends boolean = false>(
224 variant: V,
225 options?: {
226 readonly isDefault?: IsDefault | undefined
227 }
228 ): <A extends Struct<any>>(self: A) => Extract<V, A, IsDefault>
229 <V extends string, A extends Struct<any>, const IsDefault extends boolean = false>(self: A, variant: V, options?: {
230 readonly isDefault?: IsDefault | undefined
231 }): Extract<V, A, IsDefault>
232} = dual(
233 (args) => isStruct(args[0]),
234 <V extends string, A extends Struct<any>>(
235 self: A,
236 variant: V,
237 options?: {
238 readonly isDefault?: boolean | undefined
239 }
240 ): Extract<V, A> => {
241 if (options?.isDefault === true && Schema.isSchema(self)) {
242 return self as any
243 }
244 const cache = options?.isDefault === true
245 ? self[defaultCacheSymbol] ?? (self[defaultCacheSymbol] = Object.create(null))
246 : self[cacheSymbol] ?? (self[cacheSymbol] = Object.create(null))
247 if (Object.hasOwn(cache, variant)) {
248 return cache[variant] as any
249 }
250 const fields: Record<string, any> = {}
251 for (const key of Object.keys(self[TypeId])) {
252 const value = self[TypeId][key]
253 if (value === undefined) {
254 continue
255 }
256 if (TypeId in value) {
257 if (options?.isDefault === true && Schema.isSchema(value)) {
258 InternalRecord.assignProperty(fields, key, value)
259 } else {
260 InternalRecord.assignProperty(fields, key, extract(value, variant))
261 }
262 } else if (FieldTypeId in value) {
263 if (Object.hasOwn(value.schemas, variant)) {
264 const schema = value.schemas[variant]
265 if (schema !== undefined) {
266 InternalRecord.assignProperty(fields, key, schema)
267 }
268 }
269 } else {
270 InternalRecord.assignProperty(fields, key, value)
271 }
272 }
273 const schema = Schema.Struct(fields)
274 cache[variant] = schema
275 return schema as any
276 }
277)
278
279/**
280 * Returns the original field definitions stored on a variant schema struct.
281 *
282 * @stability unstable
283 * @category accessors
284 * @since 4.0.0
285 */
286export const fields = <A extends Struct<any>>(self: A): A[typeof TypeId] => self[TypeId]
287
288/**
289 * Schema class type returned by variant class constructors, combining the default
290 * variant schema with access to the original variant fields.
291 *
292 * @stability unstable
293 * @category models
294 * @since 4.0.0
295 */
296export interface Class<
297 Self,
298 Fields extends Struct.Fields,
299 S extends Schema.Top & {
300 readonly fields: Schema.Struct.Fields
301 }
302> extends Schema.Class<Self, S, {}>, Struct<Struct_.Simplify<Fields>> {
303 readonly "Type": Self
304 readonly "Encoded": S["Encoded"]
305 readonly "DecodingServices": S["DecodingServices"]
306 readonly "EncodingServices": S["EncodingServices"]
307 readonly "~type.make.in": S["~type.make.in"]
308 readonly "~type.make": Self
309 readonly "Iso": S["Iso"]
310
311 new(
312 props: S["~type.make.in"],
313 options?: {
314 readonly disableChecks?: boolean
315 } | undefined
316 ): S["Type"]
317
318 make<Args extends Array<any>, X>(
319 this: { new(...args: Args): X },
320 ...args: Args
321 ): X
322
323 readonly fields: S["fields"]
324}
325
326type MissingSelfGeneric<Params extends string = ""> =
327 `Missing \`Self\` generic - use \`class Self extends Class<Self>()(${Params}{ ... })\``
328
329/**
330 * Union schema over the default schemas of a list of variant schema structs.
331 *
332 * @stability unstable
333 * @category models
334 * @since 4.0.0
335 */
336export interface Union<Members extends ReadonlyArray<Struct<any>>, Default extends string = string>
337 extends
338 Schema.Union<
339 {
340 readonly [K in keyof Members]: Extract<Default, Members[K], true>
341 }
342 >
343{}
344
345/**
346 * Type-level helpers for unions of variant schema structs.
347 *
348 * @stability unstable
349 * @since 4.0.0
350 */
351export declare namespace Union {
352 /**
353 * Computes a union schema for each variant from a list of variant schema structs.
354 *
355 * @stability unstable
356 * @category models
357 * @since 4.0.0
358 */
359 export type Variants<Members extends ReadonlyArray<Struct<any>>, Variants extends string> = {
360 readonly [Variant in Variants]: Schema.Union<
361 {
362 [K in keyof Members]: Extract<Variant, Members[K]>
363 }
364 >
365 }
366}
367
368/**
369 * Creates a variant schema toolkit for a fixed set of variant names and a default
370 * variant.
371 *
372 * @stability unstable
373 * @category constructors
374 * @since 4.0.0
375 */
376export const make = <
377 const Variants extends ReadonlyArray<string>,
378 const Default extends Variants[number]
379>(options: {
380 readonly variants: Variants
381 readonly defaultVariant: Default
382}): {
383 readonly Struct: <const A extends Struct.Fields>(
384 fields: A & Struct.Validate<A, Variants[number]>
385 ) => Struct<A>
386 readonly Field: <const A extends Field.ConfigWithKeys<Variants[number]>>(
387 config: A & { readonly [K in Exclude<keyof A, Variants[number]>]: never }
388 ) => Field<A>
389 readonly FieldOnly: <const Keys extends ReadonlyArray<Variants[number]>>(
390 keys: Keys
391 ) => <S extends Schema.Top>(
392 schema: S
393 ) => Field<{ readonly [K in Keys[number]]: S }>
394 readonly FieldExcept: <const Keys extends ReadonlyArray<Variants[number]>>(
395 keys: Keys
396 ) => <S extends Schema.Top>(
397 schema: S
398 ) => Field<{ readonly [K in Exclude<Variants[number], Keys[number]>]: S }>
399 readonly fieldEvolve: {
400 <
401 Self extends Field<any> | Schema.Top,
402 const Mapping extends (Self extends Field<infer S> ? { readonly [K in keyof S]?: (variant: S[K]) => Schema.Top }
403 : { readonly [K in Variants[number]]?: (variant: Self) => Schema.Top })
404 >(f: Mapping): (self: Self) => Field<
405 Self extends Field<infer S> ? {
406 readonly [K in keyof S]: K extends keyof Mapping
407 ? Mapping[K] extends (arg: any) => any ? ReturnType<Mapping[K]> : S[K]
408 : S[K]
409 } :
410 {
411 readonly [K in Variants[number]]: K extends keyof Mapping
412 ? Mapping[K] extends (arg: any) => any ? ReturnType<Mapping[K]> : Self
413 : Self
414 }
415 >
416 <
417 Self extends Field<any> | Schema.Top,
418 const Mapping extends (Self extends Field<infer S> ? {
419 readonly [K in keyof S]?: (variant: S[K]) => Schema.Top
420 }
421 : { readonly [K in Variants[number]]?: (variant: Self) => Schema.Top })
422 >(self: Self, f: Mapping): Field<
423 Self extends Field<infer S> ? {
424 readonly [K in keyof S]: K extends keyof Mapping
425 ? Mapping[K] extends (arg: any) => any ? ReturnType<Mapping[K]> : S[K]
426 : S[K]
427 } :
428 {
429 readonly [K in Variants[number]]: K extends keyof Mapping
430 ? Mapping[K] extends (arg: any) => any ? ReturnType<Mapping[K]> : Self
431 : Self
432 }
433 >
434 }
435 readonly Class: <Self = never>(
436 identifier: string
437 ) => <const Fields extends Struct.Fields>(
438 fields: Fields & Struct.Validate<Fields, Variants[number]>,
439 annotations?:
440 | Schema.Annotations.Declaration<Self, readonly [Schema.Struct<ExtractFields<Default, Fields, true>>]>
441 | undefined
442 ) => [Self] extends [never] ? MissingSelfGeneric
443 :
444 & Class<
445 Self,
446 Fields,
447 Schema.Struct<ExtractFields<Default, Fields, true>>
448 >
449 & {
450 readonly [V in Variants[number]]: Extract<V, Struct<Fields>>
451 }
452 readonly Union: <const Members extends ReadonlyArray<Struct<any>>>(
453 members: Members
454 ) => Union<Members, Default> & Union.Variants<Members, Variants[number]>
455 readonly extract: {
456 <V extends Variants[number]>(
457 variant: V
458 ): <A extends Struct<any>>(self: A) => Extract<V, A, V extends Default ? true : false>
459 <V extends Variants[number], A extends Struct<any>>(
460 self: A,
461 variant: V
462 ): Extract<V, A, V extends Default ? true : false>
463 }
464} => {
465 function Class<Self>(identifier: string) {
466 return function(
467 fields: Struct.Fields,
468 annotations?: Schema.Annotations.Declaration<
469 Self,
470 readonly [Schema.Struct<ExtractFields<Default, typeof fields, true>>]
471 >
472 ) {
473 const variantStruct = Struct(fields)
474 const schema = extract(variantStruct, options.defaultVariant, {
475 isDefault: true
476 })
477 const SClass = Schema.Class as any
478 class Base extends SClass(identifier)(schema.fields, annotations) {
479 static [TypeId] = fields
480 }
481 for (const variant of options.variants) {
482 Object.defineProperty(Base, variant, {
483 value: extract(variantStruct, variant).annotate({
484 id: `${identifier}.${variant}`,
485 title: `${identifier}.${variant}`
486 })
487 })
488 }
489 return Base
490 }
491 }
492 function FieldOnly<const Keys extends ReadonlyArray<Variants[number]>>(keys: Keys) {
493 return function<S extends Schema.Top>(schema: S) {
494 const obj: Record<string, S> = {}
495 for (const key of keys) {
496 InternalRecord.assignProperty(obj, key, schema)
497 }
498 return Field(obj)
499 }
500 }
501 function FieldExcept<const Keys extends ReadonlyArray<Variants[number]>>(keys: Keys) {
502 return function<S extends Schema.Top>(schema: S) {
503 const obj: Record<string, S> = {}
504 for (const variant of options.variants) {
505 if (!keys.includes(variant)) {
506 InternalRecord.assignProperty(obj, variant, schema)
507 }
508 }
509 return Field(obj)
510 }
511 }
512 function UnionVariants(members: ReadonlyArray<Struct<any>>) {
513 return Union(members, options.defaultVariant, options.variants)
514 }
515 const fieldEvolve = dual(
516 2,
517 (
518 self: Field<any> | Schema.Top,
519 f: Record<string, (schema: Schema.Top) => Schema.Top>
520 ): Field<any> => {
521 const field = isField(self) ? self : Field(Object.fromEntries(
522 options.variants.map((variant) => [variant, self])
523 ))
524 return Field(Struct_.evolve(field.schemas, f))
525 }
526 )
527 const extractVariants = dual(
528 2,
529 (self: Struct<any>, variant: string): any =>
530 extract(self, variant, {
531 isDefault: variant === options.defaultVariant
532 })
533 )
534 return {
535 Struct,
536 Field,
537 FieldOnly,
538 FieldExcept,
539 Class,
540 Union: UnionVariants,
541 fieldEvolve,
542 // fieldFromKey,
543 extract: extractVariants
544 } as any
545}
546
547/**
548 * Marks a value as an explicit override for an `Overrideable` schema default.
549 *
550 * @stability unstable
551 * @category constructors
552 * @since 4.0.0
553 */
554export const Override = <A>(value: A): A & Brand<"Override"> => value as any
555
556/**
557 * Schema type whose constructor can use an effectful default unless a value is
558 * explicitly branded with `Override`.
559 *
560 * @stability unstable
561 * @category schemas
562 * @since 4.0.0
563 */
564export interface Overrideable<S extends Schema.Top & Schema.WithoutConstructorDefault> extends
565 Schema.BottomLazy<
566 S["ast"],
567 Overrideable<S>,
568 S["~type.parameters"],
569 S["~type.mutability"],
570 "required",
571 "with-default",
572 S["~encoded.mutability"],
573 S["~encoded.optionality"]
574 >
575{
576 readonly "Type": S["Type"] & Brand<"Override">
577 readonly "Encoded": S["Encoded"]
578 readonly "DecodingServices": S["DecodingServices"]
579 readonly "EncodingServices": S["EncodingServices"]
580 readonly "~type.make.in": S["~type.make.in"]
581 readonly "~type.make": (S["Type"] & Brand<"Override">) | undefined
582 readonly "Iso": (S["Type"] & Brand<"Override">) | undefined
583}
584
585/**
586 * Wraps a schema with an effectful constructor default while allowing explicit
587 * values to be marked with `Override`.
588 *
589 * @stability unstable
590 * @category schemas
591 * @since 4.0.0
592 */
593export const Overrideable = <S extends Schema.Top & Schema.WithoutConstructorDefault>(
594 schema: S,
595 options: {
596 readonly defaultValue: Effect.Effect<S["~type.make.in"]>
597 }
598): Overrideable<S> =>
599 schema.pipe(
600 Schema.decodeTo(Schema.brand("Override")(Schema.toType(schema))),
601 Schema.withConstructorDefault(Effect.map(options.defaultValue, Override))
602 ) as any
603
604const StructProto = {
605 pipe() {
606 return pipeArguments(this, arguments)
607 }
608}
609
610const Struct = <const A extends Field.Fields>(fields: A): Struct<A> => {
611 const self = Object.create(StructProto)
612 self[TypeId] = fields
613 return self
614}
615
616const FieldProto = {
617 [FieldTypeId]: FieldTypeId,
618 pipe() {
619 return pipeArguments(this, arguments)
620 }
621}
622
623const Field = <const A extends Field.Config>(schemas: A): Field<A> => {
624 const self = Object.create(FieldProto)
625 self.schemas = schemas
626 return self
627}
628
629const Union = <
630 Members extends ReadonlyArray<Struct<any>>,
631 Default extends string,
632 Variants extends ReadonlyArray<string>
633>(
634 members: Members,
635 defaultVariant: Default,
636 variants: Variants
637) => {
638 const VariantUnion = Schema.Union(
639 members.map((member) => Schema.isSchema(member) ? member : extract(member, defaultVariant, { isDefault: true }))
640 ) as any
641 for (const variant of variants) {
642 Object.defineProperty(VariantUnion, variant, {
643 value: Schema.Union(members.map((member) => extract(member, variant)))
644 })
645 }
646 return VariantUnion
647}