/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ import { identity } from "@effect-ts-app/core/Function" import type { AnyError, ConstructorErrorOf, ConstructorInputOf, DefaultSchema, EncodedOf, ParsedShapeOf, PropertyRecord, Schema, SchemaAny, schemaField, SchemaUPI, UnionApi } from "@effect-ts-app/schema" import { Constructor, EParserFor, findAnnotation, Guard, LongString, maxLengthIdentifier, minLengthIdentifier, named, nullableIdentifier, Parser, ReasonableString, union as unionOrig, unionIdentifier, unsafe } from "@effect-ts-app/schema" import type * as Th from "@effect-ts-app/schema/custom/These" import type { EnforceNonEmptyRecord } from "@effect-ts/core/Utils" import type * as faker from "faker" import * as S from "@effect-ts-app/schema" import { fakerToArb, getFaker } from "../faker.js" export { matchTag } from "@effect-ts/core/Utils" /** * A little helper to allow writing `interface X extends Identity` * so you don't need an intermediate type for `typeof Y` */ export type Identity = T export function fitIntoReasonableString(str: string) { if (Guard.for(ReasonableString)(str)) { return str } return ReasonableString(str.substring(0, 255 - 3) + "...") } export function fitIntoLongString(str: string) { if (Guard.for(LongString)(str)) { return str } return LongString(str.substring(0, 2047 - 3) + "...") } export class CustomSchemaException extends Error { readonly _tag = "ValidationError" readonly errors: ReadonlyArray constructor(error: S.AnyError) { super(S.drawError(error)) this.errors = [error] } toJSON() { return { message: this.message, errors: this.errors } } } export const fakerArb = ( gen: (fake: typeof faker) => () => ReturnType ): ((a: any) => S.Arbitrary.Arbitrary) => fakerToArb(gen(getFaker())) /** * The Effect fails with `CustomSchemaException` when the parser produces an invalid result. * Otherwise succeeds with the valid result. */ export function condemnCustom_( self: Parser.Parser, a: X, env?: Parser.ParserEnv ) { return Effect.suspendSucceed(() => { const res = self(a, env).effect if (res._tag === "Left") { return Effect.fail(new CustomSchemaException(res.left)) } const warn = res.right[1] if (warn._tag === "Some") { return Effect.fail(new CustomSchemaException(warn.value)) } return Effect(res.right[0]) }) } /** * The Effect fails with the generic `E` type when the parser produces an invalid result * Otherwise success with the valid result. */ export function condemn_( self: Parser.Parser, x: X, env?: Parser.ParserEnv ) { return Effect.suspend(() => { const y = self(x, env).effect if (y._tag === "Left") { return Effect.fail(y.left) } const [a, w] = y.right return w._tag === "Some" ? Effect.fail(w.value) : Effect(a) }) } export function condemnCustom(self: Parser.Parser) { return (a: X, env?: Parser.ParserEnv) => condemnCustom_(self, a, env) } export function condemnLeft_( self: Parser.Parser, a: X, env?: Parser.ParserEnv ): Either { const res = self(a, env).effect if (res._tag === "Left") { return Either.left(new CustomSchemaException(res.left)) } const warn = res.right[1] if (warn._tag === "Some") { return Either.left(new CustomSchemaException(warn.value)) } return Either(res.right[0]) } export function condemnLeft(self: Parser.Parser) { return (a: X, env?: Parser.ParserEnv) => condemnLeft_(self, a, env) } export function parseCondemnCustom_( self: Schema, a: A, env?: Parser.ParserEnv, __trace?: string ) { const parser = Parser.for(self) return condemnCustom_(parser, a, env) } export function parseECondemnCustom_( self: Schema, a: D, env?: Parser.ParserEnv, __trace?: string ) { const parser = EParserFor(self) return condemnCustom_(parser, a, env) } /** * The Effect dies with `ThrowableCondemnException` when the parser produces an invalid result. * Otherwise succeeds with the valid result. */ export function condemnDie_( self: Parser.Parser, a: X, env?: Parser.ParserEnv, __trace?: string ) { const cl = condemnLeft(self) return Effect.fromEither(cl(a, env)).orDie } export function parseCondemnDie_( self: Schema, a: A, env?: Parser.ParserEnv, __trace?: string ) { const parser = Parser.for(self) return condemnDie_(parser, a, env) } export function parseECondemnDie_( self: Schema, a: D, env?: Parser.ParserEnv, __trace?: string ) { const parser = EParserFor(self) return condemnDie_(parser, a, env) } export function parseECondemnDie(self: Schema) { const parser = EParserFor(self) return (a: D, env?: Parser.ParserEnv) => { return condemnDie_(parser, a, env) } } export function parseECondemnFail(self: Schema) { const parser = EParserFor(self) return (a: D, env?: Parser.ParserEnv) => { return condemnFail_(parser, a, env) } } export function parseECondemnLeft(self: Schema) { const parser = EParserFor(self) return (a: D, env?: Parser.ParserEnv) => { return condemnLeft_(parser, a, env) } } export function parseECondemnCustom(self: Schema) { const parser = EParserFor(self) return (a: D, env?: Parser.ParserEnv) => { return condemnCustom_(parser, a, env) } } export function parseCondemnDie(self: Schema) { const parser = Parser.for(self) return (a: A, env?: Parser.ParserEnv) => { return condemnDie_(parser, a, env) } } export function parseCondemnFail(self: Schema) { return (a: A, env?: Parser.ParserEnv) => { const parser = Parser.for(self) return condemnFail_(parser, a, env) } } export function parseCondemnLeft(self: Schema) { const parser = Parser.for(self) return (a: A, env?: Parser.ParserEnv) => { return condemnLeft_(parser, a, env) } } export function parseCondemnCustom(self: Schema) { const parser = Parser.for(self) return (a: A, env?: Parser.ParserEnv) => { return condemnCustom_(parser, a, env) } } export function parseCondemn(self: Schema) { const parser = Parser.for(self) return (a: A, env?: Parser.ParserEnv) => { return condemn_(parser, a, env) } } export function parseECondemn_( self: Schema, a: D, env?: Parser.ParserEnv, __trace?: string ) { const parser = EParserFor(self) return condemn_(parser, a, env) } export function parseECondemn(self: Schema) { const parser = EParserFor(self) return (a: D, env?: Parser.ParserEnv) => { return condemn_(parser, a, env) } } export function parseUnsafe(self: Schema) { const parser = Parser.for(self) const uns = unsafe(parser) return (a: A, env?: Parser.ParserEnv) => { return uns(a, env) } } export function parseEUnsafe(self: Schema) { const parser = EParserFor(self) const uns = unsafe(parser) return (a: D, env?: Parser.ParserEnv) => { return uns(a, env) } } /** * The Effect fails with `ThrowableCondemnException` when the parser produces an invalid result. * Otherwise succeeds with the valid result. */ export function condemnFail_( self: Parser.Parser, a: X, env?: Parser.ParserEnv, __trace?: string ) { const cl = condemnLeft(self) return Effect.fromEither(cl(a, env)) } export function parseCondemnFail_( self: Schema, a: A, env?: Parser.ParserEnv, __trace?: string ) { const parser = Parser.for(self) return condemnFail_(parser, a, env) } export function parseECondemnFail_( self: Schema, a: D, env?: Parser.ParserEnv, __trace?: string ) { const parser = EParserFor(self) return condemnFail_(parser, a, env) } export function parseCondemnLeft_( self: Schema, a: A, env?: Parser.ParserEnv ) { const parser = Parser.for(self) return condemnLeft_(parser, a, env) } export function parseECondemnLeft_( self: Schema, a: D, env?: Parser.ParserEnv ) { const parser = EParserFor(self) return condemnLeft_(parser, a, env) } export function tryParse(self: Parser.Parser) { return (a: X, env?: Parser.ParserEnv) => { const res = self(a, env).effect if (res._tag === "Left") { return Opt.none } const warn = res.right[1] if (warn._tag === "Some") { return Opt.none } return Opt(res.right[0]) } } export function isSchema( p: S.SchemaAny | S.AnyProperty ): p is S.SchemaAny { return !!(p as any)[S.SchemaSym] } export function getMetadataFromSchemaOrProp(p: S.SchemaAny | S.AnyProperty) { if (isSchema(p)) { return getMetadataFromSchema(p) } return getMetadataFromProp(p) } // 1. get metadata from properties, use it to constrain fields // 2. use the metadata for custom validation error messges? // 3. or leverage the actual validation errors that come from parsing the fields. // function getMetadataFromProp_(p: Prop) { // return { // required: p._optional === "required", // } // } export function getMetadataFromProp(p: Prop) { const schemaMetadata = getMetadataFromSchema(p._schema) // const propMetadata = getMetadataFromProp_(p) return schemaMetadata // return { // ...schemaMetadata, // required: propMetadata.required && schemaMetadata.required, // } } export function getMetadataFromSchema(self: Self) { const nullable = S.findAnnotation(self, nullableIdentifier) const realSelf = nullable?.self ?? self const minLength = S.findAnnotation(realSelf, minLengthIdentifier) const maxLength = S.findAnnotation(realSelf, maxLengthIdentifier) return { minLength: minLength?.minLength, maxLength: maxLength?.maxLength, required: !nullable } } export function getRegisterFromSchemaOrProp(p: S.SchemaAny | S.AnyProperty) { if (isSchema(p)) { return getRegisterFromSchema(p) } return getRegisterFromProp(p) } // 1. get metadata from properties, use it to constrain fields // 2. use the metadata for custom validation error messges? // 3. or leverage the actual validation errors that come from parsing the fields. export function getRegisterFromProp(p: Prop) { const schemaMetadata = getRegisterFromSchema(p._schema) // const metadata = getMetadataFromProp_(p) return { ...schemaMetadata // optional props should not translate values to undefined, as empty value is not absence // ...(!metadata.required // ? { // transform: { // output: (value: any) => (value ? value : undefined), // input: (value: any) => (!value ? "" : value), // }, // } // : {}), } } export function getRegisterFromSchema(self: Self) { // or take from openapi = number type? const numberIds = [ S.numberIdentifier, S.intIdentifier, S.intFromNumberIdentifier, S.positiveIntIdentifier, S.positiveIntFromNumberIdentifier, S.positiveIdentifier ] const metadata = getMetadataFromSchema(self) const nullable = S.findAnnotation(self, nullableIdentifier) const mapType = numberIds.some(x => S.findAnnotation(nullable?.self ?? self, x)) ? ("asNumber" as const) : ("normal" as const) const map = mapValueType(mapType) return { ...(!metadata.required ? { transform: { output: (value: any) => map(value === "" ? null : value), // for date fields we should not undo null.. // actually for string fields they appropriately convert to empty string probably anyway, so lets remove // input: (value: any) => (value === null || value === undefined ? "" : value), input: identity } } : { transform: { output: map, input: identity } }) } } function asNumber(value: any) { return value === null || value === undefined ? value : value === "" ? NaN : typeof value === "string" ? +value.replace(",", ".") : +value } function asDate(value: any) { return value === null || value === undefined ? value : new Date(value) } function mapValueType(type: "asNumber" | "asDate" | "normal") { return type === "asNumber" ? asNumber : type === "asDate" ? asDate : identity } export type SchemaFrom = Cls["Model"] export type GetProps = // Transform< Cls["Api"]["props"] export type GetProvidedProps< Cls extends { [schemaField]: { Api: { props: PropertyRecord } } } > = GetProps // Cls["ProvidedProps"] //Transform< export type EncodedFromApi = EncodedOf< Cls[schemaField] > // Transform< export type ConstructorInputFromApi = ConstructorInputOf // > // export type EncodedOf> = Transform< // EncodedOfOrig // > export type OpaqueEncoded = Schema extends DefaultSchema< unknown, infer A, infer B, OpaqueE, infer C > ? DefaultSchema : never // TODO: Add `is` guards (esp. for tagged unions.) export function smartClassUnion< T extends Record >(members: T & EnforceNonEmptyRecord, name?: string) { // @ts-expect-error we know this is NonEmpty const u = unionOrig(members) return enhanceClassUnion(u, name) } export function enhanceClassUnion< T extends Record, A, E, CI >(u: DefaultSchema>, name?: string) { if (name) u = u["|>"](named(name)) as typeof u const members = findAnnotation(u, unionIdentifier)!.props as T const entries = Object.entries(members) const as = entries.reduce((prev, [key, value]) => { prev[key] = (i: any) => new value(i) return prev }, {} as Record) as any as { [Key in keyof T]: (i: ConstructorInputOf) => A } const of = entries.reduce((prev, [key, value]) => { prev[key] = (i: any) => new value(i) return prev }, {} as Record) as any as { [Key in keyof T]: (i: ConstructorInputOf) => InstanceType } // Experiment with returning a constructor that returns a Union const cas = entries.reduce((prev, [key, value]) => { prev[key] = value return prev }, {} as Record) as any as { [Key in keyof T]: { new(i: ConstructorInputOf): A } } const mem = entries.reduce((prev, [key, value]) => { prev[key] = value return prev }, {} as Record) as any as { [Key in keyof T]: T[Key] } const of_ = (i: A): A => i const ext = { members, cas, mem, as, of, of_, EParser: EParserFor(u) } as SmartClassUnion return Object.assign(u, ext) } export interface SmartClassUnion< T extends Record, Encoded, ParsedShape > { members: T cas: { [Key in keyof T]: { new(i: ConstructorInputOf): ParsedShape } } mem: { [Key in keyof T]: T[Key] } of: { [Key in keyof T]: (i: ConstructorInputOf) => InstanceType } of_: (i: ParsedShape) => ParsedShape as: { [Key in keyof T]: (i: ConstructorInputOf) => ParsedShape } EParser: Parser.Parser } export function smartUnion>( members: T & EnforceNonEmptyRecord ) { // @ts-expect-error we know this is NonEmpty const u = unionOrig(members) return enhanceUnion(u) } export function enhanceUnion, A, E, CI>( u: DefaultSchema> ) { const members = findAnnotation(u, unionIdentifier)!.props as T const entries = Object.entries(members) // const as = entries.reduce((prev, [key, value]) => { // prev[key] = Constructor.for(value) // return prev // }, {} as Record) as any as { // [Key in keyof T]: ( // i: ConstructorInputOf // ) => Th.These, A> // } const as = entries.reduce((prev, [key, value]) => { prev[key] = Constructor.for(value) return prev }, {} as Record) as any as { [Key in keyof T]: ( i: ConstructorInputOf ) => Th.These, A> } const of = entries.reduce((prev, [key, value]) => { prev[key] = Constructor.for(value)["|>"](unsafe) return prev }, {} as Record) as any as { [Key in keyof T]: (i: ConstructorInputOf) => ParsedShapeOf // Th.These, ParsedShapeOf> } const mem = entries.reduce((prev, [key, value]) => { prev[key] = value return prev }, {} as Record) as any as { [Key in keyof T]: T[Key] } const of_ = (i: A): A => i const ext = { members, as, mem, of, of_, EParser: EParserFor(u) } as SmartUnion return Object.assign(u, ext) } export interface SmartUnion< T extends Record, Encoded, ParsedShape > { members: T mem: { [Key in keyof T]: T[Key] } of: { [Key in keyof T]: (i: ConstructorInputOf) => ParsedShapeOf } of_: (i: ParsedShape) => ParsedShape as: { [Key in keyof T]: ( i: ConstructorInputOf ) => Th.These, ParsedShape> } EParser: Parser.Parser } /** * The Either's left is returned with the parser errors when the parser produces an invalid result * Otherwise right with the valid result. */ export function validate( self: (a: X) => Th.These ): (a: X) => Either { return x => { const y = self(x).effect if (y._tag === "Left") { return Either.left(new CustomSchemaException(y.left)) } const [a, w] = y.right return w._tag === "Some" ? Either.left(new CustomSchemaException(w.value)) : Either(a) } } /** * Value: The value you want to submit after validation. e.g for text input: `ReasonableString` * InputValue: The internal value of the input, e.g for text input: `string` */ export type InputSchema = DefaultSchema< unknown, Value, any, InputValue, any > /** * The Either's left is returned with the parser errors when the parser produces an invalid result * Otherwise right with the valid result. */ export function makeValidator(self: InputSchema) { return validate(EParserFor(self)) } /** * The Either's left is returned with the parser errors when the parser produces an invalid result * Otherwise right with the valid result. */ export function makeValidatorFromUnknown( self: InputSchema ) { return validate(Parser.for(self)) } export type ParsedShapeOfCustom> = ReturnType< X["_ParsedShape"] > // TODO: Opaque UnionApi (return/input type of matchW etc?) export function OpaqueSchema() { function abc( self: DefaultSchema ): DefaultSchema & { original: OriginalA } function abc< OriginalA, ParserInput, Api, A1 extends Record, A2, A3 >( self: DefaultSchema & SmartClassUnion ): & DefaultSchema & SmartClassUnion & { original: OriginalA } // function abc< // ParserInput, // Api, // A1 extends Record, // A2, // A3 // >( // self: DefaultSchema & SmartUnion // ): DefaultSchema & SmartUnion function abc(self: any): any { return self } return abc } export interface UnionBrand {} /** * Currently does not implement composition, but is useful as a quick set/modify. * @tsplus type PreparedLens */ export class PreparedLens { constructor(private readonly s: S, readonly lens: Lens) {} get = () => this.lens.get(this.s) set = (t: T) => this.lens.set_(this.s, t) } /** * @tsplus fluent PreparedLens modify */ export function modify(l: PreparedLens, mod: (t: T) => T) { return l.set(mod(l.get())) } export function makePreparedLenses( props: Props, s: S ): { [K in keyof Props]: PreparedLens> } { function makeLens(l: Lens) { return new PreparedLens(s, l) } const id = Lens.id() return Object.keys(props).reduce((prev, cur) => { prev[cur] = makeLens(id.prop(cur as any)) return prev }, {} as any) } export * from "@effect-ts-app/schema" export * from "./overrides.js" export { array, nonEmptyArray, set } from "./overrides.js"