import * as Dictionary from "@effect-ts/core/Collections/Immutable/Dictionary" import * as HashMap from "@effect-ts/core/Collections/Immutable/HashMap" import { pipe } from "@effect-ts/core/Function" import type { Compute, UnionToIntersection } from "@effect-ts/core/Utils" import { intersect } from "@effect-ts/core/Utils" import type * as fc from "fast-check" import * as S from "../_schema.js" import type { Annotation } from "../_schema/annotation.js" import { augmentRecord } from "../_utils.js" import * as Arbitrary from "../Arbitrary.js" import * as Encoder from "../Encoder.js" import * as Guard from "../Guard.js" import * as Parser from "../Parser.js" import type { ParserEnv } from "../Parser.js" import * as Th from "../These.js" import type { LiteralApi } from "./literal.js" import type { DefaultSchema } from "./withDefaults.js" import { withDefaults } from "./withDefaults.js" /** * @tsplus type ets/Schema/Property * @tsplus companion ets/Schema/PropertyOps */ export class Property< Self extends S.SchemaUPI, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> > { constructor( readonly _as: As, readonly _schema: Self, readonly _optional: Optional, readonly _def: Def, readonly _map: HashMap.HashMap, any> ) {} // Disabled because it sends the compiler down into rabbit holes.. // schema(schema: That): Property { // return new Property(this._as, schema, this._optional, Opt.none, this._map) // } // opt(): Property { // return new Property(this._as, this._schema, "optional", this._def, this._map) // } // req(): Property { // return new Property(this._as, this._schema, "required", this._def, this._map) // } // from(as: As1): Property, Def> { // return new Property( // Opt(as), // this._schema, // this._optional, // this._def, // this._map // ) // } // removeFrom(): Property { // return new Property( // Opt.none, // this._schema, // this._optional, // this._def, // this._map // ) // } // def( // _: Optional extends "required" // ? () => S.ParsedShapeOf // : ["default can be set only for required properties", never] // ): Property S.ParsedShapeOf]>> // def( // _: Optional extends "required" // ? () => S.ParsedShapeOf // : ["default can be set only for required properties", never], // k: K // ): Property S.ParsedShapeOf]>> // def( // _: Optional extends "required" // ? () => S.ParsedShapeOf // : ["default can be set only for required properties", never], // k?: "parser" | "constructor" | "both" // ): Property< // Self, // Optional, // As, // Some<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> // > { // // @ts-expect-error // return new Property( // this._as, // this._schema, // this._optional, // // @ts-expect-error // Opt([k ?? "both", _]), // this._map // ) // } // removeDef(): Property { // return new Property(this._as, this._schema, this._optional, Opt.none, this._map) // } // getAnnotation(annotation: Annotation): Opt { // return HashMap.get_(this._map, annotation) // } // annotate(annotation: Annotation, value: A): Property { // return new Property( // this._as, // this._schema, // this._optional, // this._def, // HashMap.set_(this._map, annotation, value) // ) // } } export function propDef< Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> >( prop: Property, _: Optional extends "required" ? () => S.ParsedShapeOf : ["default can be set only for required properties", never] ): Property S.ParsedShapeOf]>> export function propDef< K extends "parser" | "constructor" | "both", Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> >( prop: Property, _: Optional extends "required" ? () => S.ParsedShapeOf : ["default can be set only for required properties", never], k: K ): Property S.ParsedShapeOf]>> export function propDef< Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> >( prop: Property, _: Optional extends "required" ? () => S.ParsedShapeOf : ["default can be set only for required properties", never], k?: "parser" | "constructor" | "both" ): Property< Self, Optional, As, Some<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> > { // @ts-expect-error return new Property( prop._as, prop._schema, prop._optional, // @ts-expect-error Opt([k ?? "both", _]), prop._map ) } export function propOpt< Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> >(prop: Property): Property { return new Property(prop._as, prop._schema, "optional", prop._def, prop._map) } export function propReq< Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]> >(prop: Property): Property { return new Property(prop._as, prop._schema, "required", prop._def, prop._map) } export function propFrom< Self extends S.SchemaAny, Optional extends "optional" | "required", As extends Opt, Def extends Opt<["parser" | "constructor" | "both", () => S.ParsedShapeOf]>, As1 extends PropertyKey >( prop: Property, as: As1 ): Property, Def> { return new Property( Opt(as) as Some, prop._schema, prop._optional, prop._def, prop._map ) } export function prop( schema: Self ): Property { return new Property( Opt.none as None, schema, "required", Opt.none as None, HashMap.make() ) } export type AnyProperty = Property export type PropertyRecord = Record export type ShapeFromProperties = Compute< UnionToIntersection< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? { readonly [h in k]?: S.ParsedShapeOf } : { readonly [h in k]: S.ParsedShapeOf } : never }[keyof Props] >, "flat" > export type ConstructorFromProperties = Compute< UnionToIntersection< { [k in keyof Props]: k extends TagsFromProps ? never : Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? { readonly [h in k]?: S.ParsedShapeOf } : Props[k]["_def"] extends Some<["constructor" | "both", any]> ? { readonly [h in k]?: S.ParsedShapeOf } : { readonly [h in k]: S.ParsedShapeOf } : never }[keyof Props] >, "flat" > export type EncodedFromProperties = Compute< UnionToIntersection< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? { readonly [ h in Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k ]?: S.EncodedOf } : { readonly [ h in Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k ]: S.EncodedOf } : never }[keyof Props] >, "flat" > export type HasRequiredProperty = unknown extends { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "required" ? unknown : never : never }[keyof Props] ? true : false export type ParserErrorFromProperties = S.CompositionE< | S.PrevE> | S.NextE< HasRequiredProperty extends true ? S.CompositionE< | S.PrevE< S.MissingKeysE< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? never : Props[k]["_def"] extends Some<["parser" | "both", any]> ? never : Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k : never }[keyof Props] > > | S.NextE< S.StructE< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? S.OptionalKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : Props[k]["_def"] extends Some<["parser" | "both", any]> ? S.OptionalKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : S.RequiredKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : never }[keyof Props] > > > : S.StructE< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? S.OptionalKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : Props[k]["_def"] extends Some<["parser" | "both", any]> ? S.OptionalKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : S.RequiredKeyE< Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k, S.ParserErrorOf > : never }[keyof Props] > > > export const propertiesIdentifier = S.makeAnnotation<{ props: PropertyRecord }>() export type SchemaProperties = DefaultSchema< unknown, ShapeFromProperties, ConstructorFromProperties, EncodedFromProperties, { props: Props } > export type TagsFromProps = { [k in keyof Props]: Props[k]["_as"] extends None ? Props[k]["_optional"] extends "required" ? S.ApiOf extends LiteralApi ? KS extends [string] ? k : never : never : never : never }[keyof Props] export function isPropertyRecord(u: unknown): u is PropertyRecord { return ( typeof u === "object" && u !== null && Object.keys(u).every(k => u[k] instanceof Property) ) } export function tagsFromProps( props: Props ): Record { const keys = Object.keys(props) const tags = {} for (const key of keys) { const s: S.SchemaUPI = props[key]._schema const def = props[key]._def as Opt< ["parser" | "constructor" | "both", () => S.ParsedShapeOf] > const as = props[key]._as as Opt if ( as.isNone() && def.isNone() && props[key]._optional === "required" && "literals" in s.Api && Array.isArray(s.Api["literals"]) && s.Api["literals"].length === 1 && typeof s.Api["literals"][0] === "string" ) { tags[key] = s.Api["literals"][0] } } return tags } export function props( props: Props ): SchemaProperties { const parsers = {} as Record> const encoders = {} const guards = {} const arbitrariesReq = {} as Record> const arbitrariesPar = {} as Record> const keys = Object.keys(props) const required = [] as string[] const defaults = [] as [string, [string, any]][] for (const key of keys) { parsers[key] = Parser.for(props[key]._schema) encoders[key] = Encoder.for(props[key]._schema) guards[key] = Guard.for(props[key]._schema) if (props[key]._optional === "required") { const def = props[key]._def as Opt< ["parser" | "constructor" | "both", () => S.ParsedShapeOf] > if (def.isNone() || (def.isSome() && def.value[0] === "constructor")) { const as = props[key]._as as Opt required.push(as.getOrElse(() => key)) } if (def.isSome() && (def.value[0] === "constructor" || def.value[0] === "both")) { defaults.push([key, def.value]) } arbitrariesReq[key] = Arbitrary.for(props[key]._schema) } else { arbitrariesPar[key] = Arbitrary.for(props[key]._schema) } } const hasRequired = required.length > 0 function guard(_: unknown): _ is ShapeFromProperties { if (typeof _ !== "object" || _ === null) { return false } for (const key of keys) { const s = props[key] if (s._optional === "required" && !(key in _)) { return false } if (key in _) { if ( (s._optional !== "optional" || typeof _[key] !== "undefined") && !guards[key](_[key]) ) { return false } } } return true } function parser( _: unknown, env?: ParserEnv ): Th.These, ShapeFromProperties> { if (typeof _ !== "object" || _ === null) { return Th.fail( S.compositionE(Chunk(S.prevE(S.leafE(S.unknownRecordE(_))))) ) } let missingKeys = Chunk.empty() for (const k of required) { if (!(k in _)) { missingKeys = missingKeys.append(k) } } if (!missingKeys.isEmpty()) { // @ts-expect-error return Th.fail( S.compositionE( Chunk( S.nextE(S.compositionE(Chunk(S.prevE(S.missingKeysE(missingKeys))))) ) ) ) } let errors = Chunk.empty< S.OptionalKeyE | S.RequiredKeyE >() let isError = false const result = {} const parsersv2 = env?.cache ? env.cache.getOrSetParsers(parsers) : parsers for (const key of keys) { const prop = props[key] const as = props[key]._as as Opt const _as: string = as.getOrElse(() => key) const def = prop._def as Opt< ["parser" | "constructor" | "both", () => S.ParsedShapeOf] > // TODO: support actual optionallity vs explicit `| undefined` if (_as in _) { const isUndefined = typeof _[_as] === "undefined" if (prop._optional === "optional" && isUndefined) { continue } if ( isUndefined && def.isSome() && // TODO: why def any // // @ts-expect-error (def.value[0] === "parser" || def.value[0] === "both") ) { // // @ts-expect-error result[key] = def.value[1]() continue } const res = parsersv2[key](_[_as]) if (res.effect._tag === "Left") { errors = errors.append( prop._optional === "required" ? S.requiredKeyE(_as, res.effect.left) : S.optionalKeyE(_as, res.effect.left) ) isError = true } else { result[key] = res.effect.right[0] const warnings = res.effect.right[1] if (warnings._tag === "Some") { errors = errors.append( prop._optional === "required" ? S.requiredKeyE(_as, warnings.value) : S.optionalKeyE(_as, warnings.value) ) } } } else { if ( def.isSome() && // // @ts-expect-error (def.value[0] === "parser" || def.value[0] === "both") ) { // // @ts-expect-error result[key] = def.value[1]() } } } if (!isError) { augmentRecord(result) } if (errors.isEmpty()) { return Th.succeed(result as ShapeFromProperties) } const error_ = S.compositionE(Chunk(S.nextE(S.structE(errors)))) const error = hasRequired ? S.compositionE(Chunk(S.nextE(error_))) : error_ if (isError) { // @ts-expect-error return Th.fail(error) } // @ts-expect-error return Th.warn(result, error) } function encoder(_: ShapeFromProperties): EncodedFromProperties { const enc = {} for (const key of keys) { if (key in _) { const as = props[key]._as as Opt const _as: string = as.getOrElse(() => key) enc[_as] = encoders[key](_[key]) } } // @ts-expect-error return enc } function arb(_: typeof fc): fc.Arbitrary> { const req = Dictionary.map_(arbitrariesReq, g => g(_)) const par = Dictionary.map_(arbitrariesPar, g => g(_)) // @ts-expect-error return _.record(req).chain(a => _.record(par, { withDeletedKeys: true }).map(b => intersect(a, b))) } const tags = tagsFromProps(props) return pipe( S.identity(guard), S.parser(parser), S.encoder(encoder), S.arbitrary(arb), S.constructor(_ => { const res = {} as ShapeFromProperties Object.assign(res, _, tags) for (const [k, v] of defaults) { if (!(k in res) || res[k] === undefined) { if (v[0] === "constructor" || v[0] === "both") { res[k] = v[1]() } } } return Th.succeed(res) }), S.mapApi(() => ({ props })), withDefaults, S.annotate(propertiesIdentifier, { props }) ) } export function propsPick( ...ks: KS ) { return ( self: Props ): Compute< UnionToIntersection< { [k in keyof Props]: k extends KS[number] ? { [h in k]: Props[h] } : never }[keyof Props] >, "flat" > => { const newProps = {} for (const k of Object.keys(self)) { if (!ks.includes(k)) { newProps[k] = self[k] } } // @ts-expect-error return newProps } } export function propsOmit( ...ks: KS ) { return ( self: Props ): Compute< UnionToIntersection< { [k in keyof Props]: k extends KS[number] ? never : { [h in k]: Props[h] } }[keyof Props] >, "flat" > => { const newProps = {} for (const k of Object.keys(self)) { if (ks.includes(k)) { newProps[k] = self[k] } } // @ts-expect-error return newProps } } export type ParserInputFromProperties = Compute< UnionToIntersection< { [k in keyof Props]: Props[k] extends AnyProperty ? Props[k]["_optional"] extends "optional" ? { readonly [ h in Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k ]?: S.EncodedOf } : Props[k]["_def"] extends Some<["parser" | "both", any]> ? { readonly [ h in Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k ]?: S.EncodedOf } : { readonly [ h in Props[k]["_as"] extends Some ? Props[k]["_as"]["value"] : k ]: S.EncodedOf } : never }[keyof Props] >, "flat" >