From 8fd543dbfeb6e109e2fe1c8108b32569daa0e206 Mon Sep 17 00:00:00 2001 From: Peter Krol Date: Sat, 15 Jun 2024 23:22:53 +0000 Subject: [PATCH] feat: add ParseError path formatter --- .changeset/dirty-hounds-mix.md | 8 + packages/base/src/collection/Iterable/api.ts | 7 + packages/base/src/collection/compat.ts | 2 + packages/base/src/collection/compat/Map.ts | 3 + .../src/collection/compat/Map/definition.ts | 6 + packages/base/src/collection/compat/Set.ts | 3 + .../src/collection/compat/Set/definition.ts | 6 + packages/base/src/data/Eq/api.ts | 23 ++ packages/base/src/data/Ord/api.ts | 35 ++ packages/base/src/util/predicates.ts | 9 + packages/http/src/BodyError.ts | 2 +- packages/http/src/IncomingMessage/api.ts | 6 +- packages/http/src/Route/api.ts | 6 +- packages/io/src/IO/api.ts | 42 +-- packages/schema/src/AST.ts | 187 +++++++--- packages/schema/src/ASTAnnotation.ts | 9 +- packages/schema/src/Gen.ts | 21 +- packages/schema/src/ParseError.ts | 338 +----------------- packages/schema/src/ParseError/ParseError.ts | 304 ++++++++++++++++ .../src/ParseError/ParseErrorFormatter.ts | 1 + .../schema/src/ParseError/PathFormatter.ts | 117 ++++++ .../schema/src/ParseError/TreeFormatter.ts | 127 +++++++ packages/schema/src/ParseFailure.ts | 18 - packages/schema/src/ParseResult.ts | 11 +- packages/schema/src/Parser/api.ts | 2 +- packages/schema/src/Parser/interpreter.ts | 173 +++++---- packages/schema/src/Schema.ts | 2 + packages/schema/src/Schema/api.ts | 22 +- packages/schema/src/Schema/api/conc.ts | 75 ++-- packages/schema/src/Schema/api/either.ts | 50 ++- packages/schema/src/Schema/api/hashMap.ts | 164 +++------ packages/schema/src/Schema/api/hashSet.ts | 148 ++------ .../schema/src/Schema/api/immutableArray.ts | 60 +--- packages/schema/src/Schema/api/list.ts | 87 ++--- packages/schema/src/Schema/api/map.ts | 93 +++++ packages/schema/src/Schema/api/maybe.ts | 53 +-- packages/schema/src/Schema/api/set.ts | 74 ++++ packages/schema/src/Show.ts | 284 ++++++++------- packages/schema/src/global.ts | 4 - packages/schema/src/utils.ts | 5 + packages/schema/test/Schema/array.test.ts | 18 +- packages/schema/test/Schema/nullable.test.ts | 25 +- .../schema/test/Schema/primitives.test.ts | 50 ++- packages/schema/test/Schema/record.test.ts | 29 ++ packages/schema/test/Schema/struct.test.ts | 18 +- packages/schema/test/Schema/union.test.ts | 36 +- packages/schema/test/utils.ts | 9 +- 47 files changed, 1617 insertions(+), 1155 deletions(-) create mode 100644 .changeset/dirty-hounds-mix.md create mode 100644 packages/base/src/collection/compat/Map.ts create mode 100644 packages/base/src/collection/compat/Map/definition.ts create mode 100644 packages/base/src/collection/compat/Set.ts create mode 100644 packages/base/src/collection/compat/Set/definition.ts create mode 100644 packages/schema/src/ParseError/ParseError.ts create mode 100644 packages/schema/src/ParseError/ParseErrorFormatter.ts create mode 100644 packages/schema/src/ParseError/PathFormatter.ts create mode 100644 packages/schema/src/ParseError/TreeFormatter.ts delete mode 100644 packages/schema/src/ParseFailure.ts create mode 100644 packages/schema/src/Schema/api/map.ts create mode 100644 packages/schema/src/Schema/api/set.ts create mode 100644 packages/schema/test/Schema/record.test.ts diff --git a/.changeset/dirty-hounds-mix.md b/.changeset/dirty-hounds-mix.md new file mode 100644 index 00000000..34e53c28 --- /dev/null +++ b/.changeset/dirty-hounds-mix.md @@ -0,0 +1,8 @@ +--- +"@fncts/schema": patch +"@fncts/base": patch +"@fncts/http": patch +"@fncts/io": patch +--- + +feat(schema): add ParseError path formatter diff --git a/packages/base/src/collection/Iterable/api.ts b/packages/base/src/collection/Iterable/api.ts index fabbaf57..48902837 100644 --- a/packages/base/src/collection/Iterable/api.ts +++ b/packages/base/src/collection/Iterable/api.ts @@ -55,6 +55,13 @@ export function toIterable(self: Iterable): Iterable { return self; } +/** + * @tsplus getter fncts.Iterable toArray + */ +export function toArray(self: Iterable): Array { + return Array.from(self); +} + /** * @tsplus pipeable fncts.Iterable flatMap */ diff --git a/packages/base/src/collection/compat.ts b/packages/base/src/collection/compat.ts index 2c94d92c..d5e5e89c 100644 --- a/packages/base/src/collection/compat.ts +++ b/packages/base/src/collection/compat.ts @@ -1,4 +1,6 @@ /* eslint-disable simple-import-sort/exports */ // codegen:start { preset: type-barrel, include: ./compat/*.ts } +export type {} from "./compat/Set.js"; +export type {} from "./compat/Map.js"; export type {} from "./compat/Array.js"; // codegen:end diff --git a/packages/base/src/collection/compat/Map.ts b/packages/base/src/collection/compat/Map.ts new file mode 100644 index 00000000..2c694275 --- /dev/null +++ b/packages/base/src/collection/compat/Map.ts @@ -0,0 +1,3 @@ +// codegen:start { preset: barrel, include: ./Map/*.ts } +export * from "./Map/definition.js"; +// codegen:end diff --git a/packages/base/src/collection/compat/Map/definition.ts b/packages/base/src/collection/compat/Map/definition.ts new file mode 100644 index 00000000..2c2004aa --- /dev/null +++ b/packages/base/src/collection/compat/Map/definition.ts @@ -0,0 +1,6 @@ +declare global { + /** + * @tsplus type fncts.Map + */ + interface Map {} +} diff --git a/packages/base/src/collection/compat/Set.ts b/packages/base/src/collection/compat/Set.ts new file mode 100644 index 00000000..a8bae6d0 --- /dev/null +++ b/packages/base/src/collection/compat/Set.ts @@ -0,0 +1,3 @@ +// codegen:start { preset: barrel, include: ./Set/*.ts } +export * from "./Set/definition.js"; +// codegen:end diff --git a/packages/base/src/collection/compat/Set/definition.ts b/packages/base/src/collection/compat/Set/definition.ts new file mode 100644 index 00000000..e78cec19 --- /dev/null +++ b/packages/base/src/collection/compat/Set/definition.ts @@ -0,0 +1,6 @@ +declare global { + /** + * @tsplus type fncts.Set + */ + interface Set {} +} diff --git a/packages/base/src/data/Eq/api.ts b/packages/base/src/data/Eq/api.ts index afffcccb..26636274 100644 --- a/packages/base/src/data/Eq/api.ts +++ b/packages/base/src/data/Eq/api.ts @@ -6,3 +6,26 @@ export function contramap(f: (b: B) => A) { return Eq({ equals: (b2) => (b1) => self.equals(f(b2))(f(b1)) }); }; } + +/** + * @tsplus static fncts.EqOps all + */ +export function all(collection: Iterable>): Eq> { + return Eq({ + equals: (y) => (x) => { + const len = Math.min(x.length, y.length); + + let collectionLength = 0; + for (const eq of collection) { + if (collectionLength >= len) { + break; + } + if (!eq.equals(y[collectionLength]!)(x[collectionLength]!)) { + return false; + } + collectionLength++; + } + return true; + }, + }); +} diff --git a/packages/base/src/data/Ord/api.ts b/packages/base/src/data/Ord/api.ts index 614dd544..3cbbc332 100644 --- a/packages/base/src/data/Ord/api.ts +++ b/packages/base/src/data/Ord/api.ts @@ -1,3 +1,38 @@ +/** + * @tsplus static fncts.OrdOps all + */ +export function all(collection: Iterable>): Ord> { + return Ord>({ + ...Eq.all(collection), + compare: (y) => (x) => { + const len = Math.min(x.length, y.length); + let collectionLength = 0; + for (const O of collection) { + if (collectionLength >= len) { + break; + } + + const o = O.compare(y[collectionLength]!)(x[collectionLength]!); + + if (o !== Ordering.EQ) { + return o; + } + collectionLength++; + } + return Ordering.EQ; + }, + }); +} + +/** + * @tsplus static fncts.OrdOps tuple + */ +export function tuple>>( + ...elements: T +): Ord] ? A : never }>> { + return Ord.all(elements) as any; +} + /* eslint-disable simple-import-sort/exports */ // codegen:start { preset: barrel, include: api/*.ts } export * from "./api/min.js"; diff --git a/packages/base/src/util/predicates.ts b/packages/base/src/util/predicates.ts index 6f91acd2..a270e39c 100644 --- a/packages/base/src/util/predicates.ts +++ b/packages/base/src/util/predicates.ts @@ -86,6 +86,15 @@ export function isPlain(value: unknown): value is object { return isObject(value) && value.constructor === Object; } +/** + * @tsplus pipeable global hasProperty + */ +export function hasProperty

(property: P) { + return (self: unknown): self is { [K in P]: unknown } => { + return isObject(self) && property in self; + }; +} + export function isInstanceOf, A>(type: C): (value: unknown) => value is A { return (value): value is A => value instanceof type; } diff --git a/packages/http/src/BodyError.ts b/packages/http/src/BodyError.ts index 7006b60a..7d4f7e84 100644 --- a/packages/http/src/BodyError.ts +++ b/packages/http/src/BodyError.ts @@ -23,7 +23,7 @@ export class JsonError extends BodyError { export class SchemaError extends BodyError { readonly _tag = BodyErrorTag.SchemaError; - constructor(readonly error: ParseFailure) { + constructor(readonly error: ParseError) { super(); } } diff --git a/packages/http/src/IncomingMessage/api.ts b/packages/http/src/IncomingMessage/api.ts index b43817ad..72c0f4b3 100644 --- a/packages/http/src/IncomingMessage/api.ts +++ b/packages/http/src/IncomingMessage/api.ts @@ -5,7 +5,7 @@ import type { IncomingMessage } from "./definition.js"; */ export function schemaBodyJson(schema: Schema) { const decode = schema.decode; - return (self: IncomingMessage): IO => self.json.flatMap(decode); + return (self: IncomingMessage): IO => self.json.flatMap(decode); } /** @@ -13,7 +13,7 @@ export function schemaBodyJson(schema: Schema) { */ export function schemaBodyUrlParams(schema: Schema) { const decode = schema.decode; - return (self: IncomingMessage): IO => self.urlParamsBody.flatMap(decode); + return (self: IncomingMessage): IO => self.urlParamsBody.flatMap(decode); } /** @@ -21,5 +21,5 @@ export function schemaBodyUrlParams(schema: Schema) { */ export function schemaHeaders(schema: Schema) { const decode = schema.decode; - return (self: IncomingMessage): IO => decode(self.headers); + return (self: IncomingMessage): IO => decode(self.headers); } diff --git a/packages/http/src/Route/api.ts b/packages/http/src/Route/api.ts index cf13436c..0dbb6be6 100644 --- a/packages/http/src/Route/api.ts +++ b/packages/http/src/Route/api.ts @@ -17,7 +17,7 @@ export const searchParams = IO.service(RouteContext.Tag).map((routeContext) => r /** * @tsplus static fncts.http.RouteContextOps schemaParams */ -export function schemaParams(schema: Schema): IO { +export function schemaParams(schema: Schema): IO { const decode = schema.decode; return IO.service(RouteContext.Tag).flatMap((routeContext) => decode({ ...routeContext.params, ...routeContext.searchParams }), @@ -27,7 +27,7 @@ export function schemaParams(schema: Schema): IO(schema: Schema): IO { +export function schemaPathParams(schema: Schema): IO { const decode = schema.decode; return params.flatMap(decode); } @@ -35,7 +35,7 @@ export function schemaPathParams(schema: Schema): IO(schema: Schema): IO { +export function schemaSearchParams(schema: Schema): IO { const decode = schema.decode; return searchParams.flatMap(decode); } diff --git a/packages/io/src/IO/api.ts b/packages/io/src/IO/api.ts index 9fc709ae..7e20731e 100644 --- a/packages/io/src/IO/api.ts +++ b/packages/io/src/IO/api.ts @@ -22,8 +22,8 @@ export function async( __tsplusTrace?: string, ): IO { const io = new IOPrimitive(IOTag.Async) as any; - io.i0 = register; - io.i1 = () => blockingOn; + io.i0 = register; + io.i1 = () => blockingOn; io.trace = __tsplusTrace; return io; } @@ -316,8 +316,8 @@ export function checkInterruptible( export function flatMap(f: (a: A) => IO, __tsplusTrace?: string) { return (ma: IO): IO => { const io = new IOPrimitive(IOTag.OnSuccess) as any; - io.i0 = ma; - io.i1 = f; + io.i0 = ma; + io.i1 = f; io.trace = __tsplusTrace; return io; @@ -365,8 +365,8 @@ export function condIO( /** * Returns a lazily constructed effect, whose construction may itself require - * effects. The effect must not throw any exceptions. When no environment is required (i.e., when R == unknown) - * it is conceptually equivalent to `flatten(succeedWith(io))`. If you wonder if the effect throws exceptions, + * effects. The effect must not throw any exceptions. When no environment is required (i.e., when R == never) + * it is conceptually equivalent to `flatten(succeed(io))`. If you wonder if the effect throws exceptions, * do not use this method, use `IO.deferTryCatch`. * * @tsplus static fncts.io.IOOps defer @@ -377,7 +377,7 @@ export function defer(io: Lazy>, __ /** * Returns a lazily constructed effect, whose construction may itself require effects. - * When no environment is required (i.e., when R == unknown) it is conceptually equivalent to `flatten(try(io))`. + * When no environment is required (i.e., when R == never) it is conceptually equivalent to `flatten(tryCatch(io))`. * * @tsplus static fncts.io.IOOps deferTry */ @@ -398,7 +398,7 @@ export function deferTry( * Returns a lazily constructed effect, whose construction may itself require effects, * translating any thrown exceptions into typed failed effects and mapping the error. * - * When no environment is required (i.e., when R == unknown) it is conceptually equivalent to `flatten(effect(io))`. + * When no environment is required (i.e., when R == never) it is conceptually equivalent to `flatten(try(io))`. * * @tsplus static IOOps deferTryCatch */ @@ -469,7 +469,7 @@ export function failNow(e: E, __tsplusTrace?: string): FIO { */ export function refailCause(cause: Cause, __tsplusTrace?: string): FIO { const io = new IOPrimitive(IOTag.Fail) as any; - io.i0 = () => cause; + io.i0 = () => cause; io.trace = __tsplusTrace; return io; } @@ -481,7 +481,7 @@ export function refailCause(cause: Cause, __tsplusTrace?: string): FIO(cause: Cause, __tsplusTrace?: string): FIO { const io = new IOPrimitive(IOTag.Fail) as any; - io.i0 = () => cause; + io.i0 = () => cause; io.trace = __tsplusTrace; return io; } @@ -1249,9 +1249,9 @@ export function matchCauseIO( ) { return (self: IO): IO => { const io = new IOPrimitive(IOTag.OnSuccessAndFailure) as any; - io.i0 = self; - io.i1 = onFailure; - io.i2 = onSuccess; + io.i0 = self; + io.i1 = onFailure; + io.i2 = onSuccess; io.trace = __tsplusTrace; return io; }; @@ -1695,7 +1695,7 @@ export function sequenceIterableDiscard(as: Iterable>, __ts */ export function succeedNow(value: A, __tsplusTrace?: string): IO { const io = new IOPrimitive(IOTag.SucceedNow) as any; - io.i0 = value; + io.i0 = value; io.trace = __tsplusTrace; return io; } @@ -1703,14 +1703,14 @@ export function succeedNow(value: A, __tsplusTrace?: string): IO(effect: Lazy, __tsplusTrace?: string): UIO { const io = new IOPrimitive(IOTag.Sync) as any; - io.i0 = effect; + io.i0 = effect; io.trace = __tsplusTrace; return io; } @@ -1723,7 +1723,7 @@ export function summarized(summary: IO, f: (start: B, e return gen(function* (_) { const start = yield* _(summary); const value = yield* _(ma); - const end = yield* _(summary); + const end = yield* _(summary); return tuple(f(start, end), value); }); }; @@ -1964,7 +1964,7 @@ export function withFiberRuntime( __tsplusTrace?: string, ): IO { const io = new IOPrimitive(IOTag.Stateful) as any; - io.i0 = onState; + io.i0 = onState; io.trace = __tsplusTrace; return io; } @@ -1974,7 +1974,7 @@ export function withFiberRuntime( */ export function updateRuntimeFlags(patch: RuntimeFlags.Patch, __tsplusTrace?: string): IO { const io = new IOPrimitive(IOTag.UpdateRuntimeFlags) as any; - io.i0 = patch; + io.i0 = patch; io.trace = __tsplusTrace; return io; } @@ -2051,8 +2051,8 @@ export function gen, A>( ): IO<_R, _E, A> { return IO.defer(() => { const iterator = f(adapter as any); - const state = iterator.next(); - const run = (state: IteratorYieldResult | IteratorReturnResult): IO => { + const state = iterator.next(); + const run = (state: IteratorYieldResult | IteratorReturnResult): IO => { if (state.done) { return IO.succeed(state.value); } diff --git a/packages/schema/src/AST.ts b/packages/schema/src/AST.ts index 8a49aae3..e7dabb40 100644 --- a/packages/schema/src/AST.ts +++ b/packages/schema/src/AST.ts @@ -22,6 +22,10 @@ export abstract class AST extends Annotated { readonly [ASTTypeId]: ASTTypeId = ASTTypeId; abstract clone(newProperties: Partial): AST; + + toString(verbose: boolean = false): string { + return this.show(verbose); + } } export declare namespace AST { @@ -103,14 +107,19 @@ export function getAnnotations(key: ASTAnnotation) { * Declaration */ +/** + * @tsplus type fncts.schema.AST.Declaration + */ export class Declaration extends AST { readonly _tag = ASTTag.Declaration; constructor( readonly typeParameters: Vector, - readonly type: AST, readonly decode: ( ...typeParameters: ReadonlyArray ) => (input: any, options?: ParseOptions) => ParseResult, + readonly encode: ( + ...typeParameters: ReadonlyArray + ) => (input: any, options?: ParseOptions) => ParseResult, readonly annotations: ASTAnnotationMap = ASTAnnotationMap.empty, ) { super(); @@ -119,8 +128,8 @@ export class Declaration extends AST { clone(newProperties: Partial): AST { return new Declaration( newProperties.typeParameters ?? this.typeParameters, - newProperties.type ?? this.type, newProperties.decode ?? this.decode, + newProperties.encode ?? this.encode, newProperties.annotations ?? this.annotations, ); } @@ -131,11 +140,11 @@ export class Declaration extends AST { */ export function createDeclaration( typeParameters: Vector, - type: AST, decode: (...typeParameters: ReadonlyArray) => (input: any, options?: ParseOptions) => ParseResult, + encode: (...typeParameters: ReadonlyArray) => (input: any, options?: ParseOptions) => ParseResult, annotations: ASTAnnotationMap = ASTAnnotationMap.empty, ): Declaration { - return new Declaration(typeParameters, type, decode, annotations); + return new Declaration(typeParameters, decode, encode, annotations); } /** @@ -528,6 +537,15 @@ export class TemplateLiteralSpan { readonly type: StringKeyword | NumberKeyword, readonly literal: string, ) {} + + toString() { + switch (this.type._tag) { + case ASTTag.StringKeyword: + return "${string}"; + case ASTTag.NumberKeyword: + return "${number}"; + } + } } /* @@ -577,6 +595,10 @@ export class Element { readonly type: AST, readonly isOptional: boolean, ) {} + + toString() { + return String(this.type) + (this.isOptional ? "?" : ""); + } } /** @@ -632,18 +654,16 @@ export const unknownArray = AST.createTuple(Vector.empty(), Just(Vector(AST.unkn * PropertySignature */ -export class PropertySignature extends AST { +export class PropertySignature { constructor( readonly name: PropertyKey, readonly type: AST, readonly isOptional: boolean, readonly isReadonly: boolean, readonly annotations: ASTAnnotationMap = ASTAnnotationMap.empty, - ) { - super(); - } + ) {} - clone(newProperties: Partial): AST { + clone(newProperties: Partial): PropertySignature { return new PropertySignature( newProperties.name ?? this.name, newProperties.type ?? this.type, @@ -705,7 +725,7 @@ export class TypeLiteral extends AST { ) { super(); this.propertySignatures = sortByAscendingCardinality(propertySignatures); - this.indexSignatures = sortByAscendingCardinality(indexSignatures); + this.indexSignatures = sortByAscendingCardinality(indexSignatures); } clone(newProperties: Partial): AST { @@ -923,6 +943,7 @@ export class Validation extends AST { ) { super(); } + clone(newProperties: Partial): AST { return new Validation( newProperties.from ?? this.from, @@ -949,8 +970,6 @@ export function createValidation( export function getCardinality(ast: AST): number { concrete(ast); switch (ast._tag) { - case ASTTag.Declaration: - return getCardinality(ast.type); case ASTTag.NeverKeyword: return 0; case ASTTag.Literal: @@ -966,7 +985,7 @@ export function getCardinality(ast: AST): number { case ASTTag.SymbolKeyword: return 3; case ASTTag.ObjectKeyword: - return 4; + return 5; case ASTTag.UnknownKeyword: case ASTTag.AnyKeyword: return 6; @@ -975,7 +994,7 @@ export function getCardinality(ast: AST): number { case ASTTag.Transform: return getCardinality(ast.to); default: - return 5; + return 4; } } @@ -983,30 +1002,64 @@ function sortByAscendingCardinality(types: Vec return types.sort(Number.Ord.contramap(({ type }) => getCardinality(type))); } -export function getWeight(ast: AST): number { +export type Weight = readonly [number, number, number]; + +const OrdWeight = Ord.tuple(Number.Ord, Number.Ord, Number.Ord); + +const maxWeight = Ord.max(OrdWeight); + +function maxWeightAll(weights: Iterable): Weight { + return weights.foldLeft(emptyWeight, (b, a) => maxWeight(b)(a)); +} + +const emptyWeight: Weight = [0, 0, 0]; + +export function getWeight(ast: AST): Weight { concrete(ast); switch (ast._tag) { case ASTTag.Declaration: - return getWeight(ast.type); + return ast.annotations.get(ASTAnnotation.Surrogate).match( + () => [6, 0, 0], + (ast) => { + const [_, y, z] = getWeight(ast); + return [6, y, z]; + }, + ); case ASTTag.Tuple: - return ast.elements.length + (ast.rest.isJust() ? ast.rest.value.length : 0); - case ASTTag.TypeLiteral: - return ast.propertySignatures.length + ast.indexSignatures.length; + return [ + 2, + ast.elements.length, + ast.rest.match( + () => 0, + (rest) => rest.length, + ), + ]; + case ASTTag.TypeLiteral: { + const y = ast.propertySignatures.length; + const z = ast.indexSignatures.length; + return y + z === 0 ? [-4, 0, 0] : [4, y, z]; + } case ASTTag.Union: - return ast.types.foldLeft(0, (n, member) => n + getWeight(member)); + return maxWeightAll(ast.types.map(getWeight)); case ASTTag.Lazy: - return 10; + return [8, 0, 0]; case ASTTag.Refinement: - return getWeight(ast.from); + const [x, y, z] = getWeight(ast.from); + return [x + 1, y, z]; case ASTTag.Transform: - return getWeight(ast.to); + return getWeight(ast.from); + case ASTTag.ObjectKeyword: + return [-2, 0, 0]; + case ASTTag.UnknownKeyword: + case ASTTag.AnyKeyword: + return [-4, 0, 0]; default: - return 0; + return emptyWeight; } } function sortByDescendingWeight(types: Vector): Vector { - return types.sort(Number.Ord.contramap(getWeight)); + return types.sort(OrdWeight.contramap(getWeight)); } function unify(candidates: Vector): Vector { @@ -1094,7 +1147,10 @@ export function getPropertySignatures(self: AST): Vector { concrete(self); switch (self._tag) { case ASTTag.Declaration: - return getPropertySignatures(self.type); + return self.annotations.get(ASTAnnotation.Surrogate).match( + () => Vector.empty(), + (surrogate) => getPropertySignatures(surrogate), + ); case ASTTag.Tuple: return self.elements.mapWithIndex((i, element) => createPropertySignature(i, element.type, element.isOptional, self.isReadonly), @@ -1116,6 +1172,8 @@ export function getPropertySignatures(self: AST): Vector { return Nothing(); }); } + case ASTTag.TypeLiteral: + return self.propertySignatures; case ASTTag.Lazy: return getPropertySignatures(self.getAST()); case ASTTag.Refinement: @@ -1134,7 +1192,10 @@ export function keysOf(ast: AST): Vector { concrete(ast); switch (ast._tag) { case ASTTag.Declaration: - return keysOf(ast.type); + return ast.annotations.get(ASTAnnotation.Surrogate).match( + () => Vector.empty(), + (surrogate) => keysOf(surrogate), + ); case ASTTag.NeverKeyword: case ASTTag.AnyKeyword: return Vector(stringKeyword, numberKeyword, symbolKeyword); @@ -1171,14 +1232,11 @@ export function keyof(self: AST): AST { */ export function createRecord(key: AST, value: AST, isReadonly: boolean): TypeLiteral { const propertySignatures: MutableVector = Vector.emptyPushable(); - const indexSignatures: MutableVector = Vector.emptyPushable(); + const indexSignatures: MutableVector = Vector.emptyPushable(); function go(key: AST): void { concrete(key); switch (key._tag) { - case ASTTag.Declaration: - go(key.type); - break; case ASTTag.NeverKeyword: break; case ASTTag.StringKeyword: @@ -1236,8 +1294,6 @@ export function omit(keys: Vector) { export function partial(self: AST): AST { concrete(self); switch (self._tag) { - case ASTTag.Declaration: - return partial(self.type); case ASTTag.Tuple: return createTuple( self.elements.map((e) => createElement(e.type, true)), @@ -1275,8 +1331,13 @@ export function createKey(key: PropertyKey): AST { export function getFrom(ast: AST): AST { AST.concrete(ast); switch (ast._tag) { - case ASTTag.Declaration: - return AST.createDeclaration(ast.typeParameters.map(getFrom), ast.type, ast.decode, ast.annotations); + case ASTTag.Declaration: { + const surrogate = ast.annotations.get(ASTAnnotation.Surrogate); + if (surrogate.isJust()) { + return getFrom(surrogate.value); + } + break; + } case ASTTag.Tuple: return AST.createTuple( ast.elements.map((element) => AST.createElement(getFrom(element.type), element.isOptional)), @@ -1309,8 +1370,13 @@ export function getFrom(ast: AST): AST { export function getTo(ast: AST): AST { AST.concrete(ast); switch (ast._tag) { - case ASTTag.Declaration: - return AST.createDeclaration(ast.typeParameters.map(getTo), ast.type, ast.decode, ast.annotations); + case ASTTag.Declaration: { + const surrogate = ast.annotations.get(ASTAnnotation.Surrogate); + if (surrogate.isJust()) { + return getTo(surrogate.value); + } + break; + } case ASTTag.Tuple: return AST.createTuple( ast.elements.map((element) => AST.createElement(getTo(element.type), element.isOptional)), @@ -1352,8 +1418,13 @@ export function getCompiler(match: AST.Match): AST.Compiler { export function getLiterals(ast: AST, isDecoding: boolean): ReadonlyArray<[PropertyKey, Literal]> { AST.concrete(ast); switch (ast._tag) { - case ASTTag.Declaration: - return getLiterals(ast.type, isDecoding); + case ASTTag.Declaration: { + const surrogate = ast.annotations.get(ASTAnnotation.Surrogate); + if (surrogate.isJust()) { + return getLiterals(surrogate.value, isDecoding); + } + break; + } case ASTTag.TypeLiteral: { const out: Array<[PropertyKey, Literal]> = []; for (let i = 0; i < ast.propertySignatures.length; i++) { @@ -1379,6 +1450,7 @@ export function getSearchTree( keys: { readonly [key: PropertyKey]: { buckets: { [literal: string]: ReadonlyArray }; + literals: ReadonlyArray; ast: AST; }; }; @@ -1387,28 +1459,31 @@ export function getSearchTree( const keys: { [key: PropertyKey]: { buckets: { [literal: string]: Array }; + literals: Array; ast: AST; }; } = {}; const otherwise: Array = []; for (let i = 0; i < members.length; i++) { const member = members[i]!; - const tags = getLiterals(member, isDecoding); + const tags = getLiterals(member, isDecoding); if (tags.length > 0) { for (let j = 0; j < tags.length; j++) { const [key, literal] = tags[j]!; - const hash = String(literal.literal); - keys[key]! ||= { buckets: {}, ast: AST.neverKeyword }; - const buckets = keys[key]!.buckets; + const hash = String(literal.literal); + keys[key]! ||= { buckets: {}, ast: AST.neverKeyword, literals: [] }; + const buckets = keys[key]!.buckets; if (Object.prototype.hasOwnProperty.call(buckets, hash)) { if (j < tags.length - 1) { continue; } buckets[hash]!.push(member); keys[key]!.ast = AST.createUnion(Vector(keys[key]!.ast, literal)); + keys[key]!.literals.push(literal); } else { buckets[hash]! = [member]; keys[key]!.ast = AST.createUnion(Vector(keys[key]!.ast, literal)); + keys[key]!.literals.push(literal); break; } } @@ -1418,3 +1493,29 @@ export function getSearchTree( } return { keys, otherwise }; } + +/** + * @tsplus pipeable fncts.schema.AST getFormattedExpected + */ +export function getFormattedExpected(verbose: boolean = false) { + return (self: AST): Maybe => { + if (verbose) { + const description = self.annotations + .get(ASTAnnotation.Description) + .orElse(self.annotations.get(ASTAnnotation.Title)); + return self.annotations.get(ASTAnnotation.Identifier).match( + () => description, + (identifier) => + description.match( + () => Just(identifier), + (description) => Just(`${identifier} (${description})`), + ), + ); + } else { + return self.annotations + .get(ASTAnnotation.Identifier) + .orElse(self.annotations.get(ASTAnnotation.Title)) + .orElse(self.annotations.get(ASTAnnotation.Description)); + } + }; +} diff --git a/packages/schema/src/ASTAnnotation.ts b/packages/schema/src/ASTAnnotation.ts index c118f4ad..08ad1fd9 100644 --- a/packages/schema/src/ASTAnnotation.ts +++ b/packages/schema/src/ASTAnnotation.ts @@ -53,7 +53,7 @@ export const DescriptionTag = Tag(); */ export const Description = new ASTAnnotation(DescriptionTag, "Description", (_, a) => a); -export const MessageTag = Tag<(_: unknown) => string>(); +export const MessageTag = Tag<(error: ParseError) => string>(); /** * @tsplus static fncts.schema.ASTAnnotationOps Message @@ -81,6 +81,13 @@ export const ParseOptionalTag = Tag(); */ export const ParseOptional = new ASTAnnotation(ParseOptionalTag, "ParseOptional", (_, b) => b); +export const SurrogateTag = Tag(); + +/** + * @tsplus static fncts.schema.ASTAnnotationOps Surrogate + */ +export const Surrogate = new ASTAnnotation(SurrogateTag, "Surrogate", (_, b) => b); + export type Hook = (...typeParameters: ReadonlyArray) => A; export function hook(handler: (...typeParameters: ReadonlyArray) => any): Hook { diff --git a/packages/schema/src/Gen.ts b/packages/schema/src/Gen.ts index 0ee52ef8..91531657 100644 --- a/packages/schema/src/Gen.ts +++ b/packages/schema/src/Gen.ts @@ -44,7 +44,10 @@ const go = memoize(function go(ast: AST): Gen { switch (ast._tag) { case ASTTag.Declaration: return getHook(ast).match( - () => go(ast.type), + () => + Gen.fromIO( + IO.haltNow(new InvalidInterpretationError("cannot build a Gen for a Declaration without a Gen hook")), + ), (hook) => hook(...ast.typeParameters.map(go)), ); case ASTTag.Literal: @@ -83,8 +86,8 @@ const go = memoize(function go(ast: AST): Gen { } case ASTTag.Tuple: { const elements = ast.elements.map((e) => go(e.type)); - const rest = ast.rest.map((restElement) => restElement.map(go)); - let output = Gen.tuple(...elements); + const rest = ast.rest.map((restElement) => restElement.map(go)); + let output = Gen.tuple(...elements); if (elements.length > 0 && rest.isNothing()) { const firstOptionalIndex = ast.elements.findIndex((e) => e.isOptional); if (firstOptionalIndex !== -1) { @@ -96,7 +99,7 @@ const go = memoize(function go(ast: AST): Gen { if (rest.isJust()) { const head = rest.value.unsafeHead!; const tail = rest.value.tail; - output = output.flatMap((as) => head.arrayWith({ maxLength: 5 }).map((rest) => [...as, ...rest])); + output = output.flatMap((as) => head.arrayWith({ maxLength: 5 }).map((rest) => [...as, ...rest])); for (let j = 0; j < tail.length; j++) { output = output.flatMap((as) => tail[j]!.map((a) => [...as, a])); } @@ -105,11 +108,11 @@ const go = memoize(function go(ast: AST): Gen { } case ASTTag.TypeLiteral: { const propertySignatureTypes = ast.propertySignatures.map((ps) => go(ps.type)); - const indexSignatures = ast.indexSignatures.map((is) => [go(is.parameter), go(is.type)] as const); + const indexSignatures = ast.indexSignatures.map((is) => [go(is.parameter), go(is.type)] as const); const requiredGens: Record> = {}; const optionalGens: Record> = {}; for (let i = 0; i < propertySignatureTypes.length; i++) { - const ps = ast.propertySignatures[i]!; + const ps = ast.propertySignatures[i]!; const name = ps.name; if (!ps.isOptional) { requiredGens[name] = propertySignatureTypes[i]!; @@ -120,8 +123,8 @@ const go = memoize(function go(ast: AST): Gen { let output = Gen.struct(requiredGens).zipWith(Gen.partial(optionalGens), (a, b) => ({ ...a, ...b })); for (let i = 0; i < indexSignatures.length; i++) { const parameter = indexSignatures[i]![0]!; - const type = indexSignatures[i]![1]!; - output = output.flatMap((o) => { + const type = indexSignatures[i]![1]!; + output = output.flatMap((o) => { return record(parameter, type).map((d) => ({ ...d, ...o })); }); } @@ -134,7 +137,7 @@ const go = memoize(function go(ast: AST): Gen { case ASTTag.Lazy: { return getHook(ast).match( () => { - const f = () => go(ast.getAST()); + const f = () => go(ast.getAST()); const get = memoize>(f); return Gen.defer(() => get(f)); }, diff --git a/packages/schema/src/ParseError.ts b/packages/schema/src/ParseError.ts index 6151e870..455f615f 100644 --- a/packages/schema/src/ParseError.ts +++ b/packages/schema/src/ParseError.ts @@ -1,331 +1,7 @@ -import type { Refinement, TemplateLiteral, TemplateLiteralSpan, Transform, Validation } from "@fncts/schema/AST"; - -import { showWithOptions } from "@fncts/base/data/Showable"; -import { concrete } from "@fncts/schema/AST"; -import { ASTTag } from "@fncts/schema/AST"; -import { ASTAnnotation } from "@fncts/schema/ASTAnnotation"; - -export const enum ParseErrorTag { - Type, - Index, - Key, - Missing, - Unexpected, - UnionMember, - Refinement, - Transformation, -} - -/** - * @tsplus type fncts.schema.ParseError - * @tsplus companion fncts.schema.ParseErrorOps - */ -export type ParseError = - | TypeError - | IndexError - | KeyError - | MissingError - | UnexpectedError - | UnionMemberError - | RefinementError - | TransformationError; - -/** - * @tsplus companion fncts.schema.ParseError.TypeError - */ -export class TypeError { - readonly _tag = ParseErrorTag.Type; - constructor( - readonly expected: AST, - readonly actual: unknown, - ) {} -} - -/** - * @tsplus static fncts.schema.ParseError.TypeError __call - * @tsplus static fncts.schema.ParseErrorOps TypeError - */ -export function typeError(expected: AST, actual: unknown): ParseError { - return new TypeError(expected, actual); -} - -/** - * @tsplus companion fncts.schema.ParseError.IndexError - */ -export class IndexError { - readonly _tag = ParseErrorTag.Index; - constructor( - readonly index: number, - readonly errors: Vector, - ) {} -} - -/** - * @tsplus static fncts.schema.ParseError.IndexError __call - * @tsplus static fncts.schema.ParseErrorOps IndexError - */ -export function indexError(index: number, errors: Vector): ParseError { - return new IndexError(index, errors); -} - -/** - * @tsplus companion fncts.schema.ParseError.KeyError - */ -export class KeyError { - readonly _tag = ParseErrorTag.Key; - constructor( - readonly keyAST: AST, - readonly key: any, - readonly errors: Vector, - ) {} -} - -/** - * @tsplus static fncts.schema.ParseError.KeyError __call - * @tsplus static fncts.schema.ParseErrorOps KeyError - */ -export function keyError(keyAST: AST, key: any, errors: Vector): ParseError { - return new KeyError(keyAST, key, errors); -} - -/** - * @tsplus companion fncts.schema.ParseError.MissingError - */ -export class MissingError { - readonly _tag = ParseErrorTag.Missing; -} - -/** - * @tsplus static fncts.schema.ParseErrorOps MissingError - */ -export const missingError = new MissingError(); - -/** - * @tsplus companion fncts.schema.ParseError.UnexpectedError - */ -export class UnexpectedError { - readonly _tag = ParseErrorTag.Unexpected; - constructor(readonly actual: unknown) {} -} - -/** - * @tsplus static fncts.schema.ParseError.UnexpectedError __call - * @tsplus static fncts.schema.ParseErrorOps UnexpectedError - */ -export function unexpectedError(actual: unknown): ParseError { - return new UnexpectedError(actual); -} - -/** - * @tsplus companion fncts.schema.ParseError.UnionMemberError - */ -export class UnionMemberError { - readonly _tag = ParseErrorTag.UnionMember; - constructor(readonly errors: Vector) {} -} - -/** - * @tsplus static fncts.schema.ParseError.UnionMemberError __call - * @tsplus static fncts.schema.ParseErrorOps UnionMemberError - */ -export function unionMemberError(errors: Vector): ParseError { - return new UnionMemberError(errors); -} - -/** - * @tsplus companion fncts.schema.ParseError.RefinementError - */ -export class RefinementError { - readonly _tag = ParseErrorTag.Refinement; - constructor( - readonly ast: Refinement, - readonly actual: unknown, - readonly kind: "From" | "Predicate", - readonly errors: Vector, - ) {} -} - -/** - * @tsplus static fncts.schema.ParseError.RefinementError __call - * @tsplus static fncts.schema.ParseErrorOps RefinementError - */ -export function refinementError( - ast: Refinement, - actual: unknown, - kind: "From" | "Predicate", - errors: Vector, -): ParseError { - return new RefinementError(ast, actual, kind, errors); -} - -/** - * @tsplus companion fncts.schema.ParseError.TransformationError - */ -export class TransformationError { - readonly _tag = ParseErrorTag.Transformation; - constructor( - readonly ast: Transform, - readonly actual: unknown, - readonly kind: "Encoded" | "Transformation" | "Type", - readonly errors: Vector, - ) {} -} - -/** - * @tsplus static fncts.schema.ParseError.TransformationError __call - * @tsplus static fncts.schema.ParseErrorOps TransformationError - */ -export function transformationError( - ast: Transform, - actual: unknown, - kind: "Encoded" | "Transformation" | "Type", - errors: Vector, -): ParseError { - return new TransformationError(ast, actual, kind, errors); -} - -/** - * @tsplus static fncts.schema.ParseErrorOps format - */ -export function format(errors: Vector): string { - return RoseTree(`${errors.length} error(s) found`, errors.map(go)).draw; -} - -function formatActual(actual: unknown): string { - return showWithOptions(actual, { colors: false }); -} - -function formatTemplateLiteralSpan(span: TemplateLiteralSpan): string { - switch (span.type._tag) { - case ASTTag.StringKeyword: - return "${string}"; - case ASTTag.NumberKeyword: - return "${number}"; - } -} - -function formatTemplateLiteral(ast: TemplateLiteral): string { - return ast.head + ast.spans.map((span) => formatTemplateLiteralSpan(span) + span.literal).join(""); -} - -function formatRefinementKind(error: RefinementError): string { - switch (error.kind) { - case "From": { - return "From side refinement failure"; - } - case "Predicate": { - return "Predicate refinement failure"; - } - } -} - -function formatTransformationKind(error: TransformationError): string { - switch (error.kind) { - case "Encoded": { - return "Encoded side transformation failure"; - } - case "Transformation": { - return "Transformation process failure"; - } - case "Type": { - return "Type side transformation failure"; - } - } -} - -function getExpected(ast: AST): Maybe { - return ast.annotations - .get(ASTAnnotation.Identifier) - .orElse(ast.annotations.get(ASTAnnotation.Title)) - .flatMap((title) => - ast.annotations.get(ASTAnnotation.Description).match( - () => Just(title), - (description) => Just(`${title} (${description})`), - ), - ); -} - -function getMissedBrands(ast: Validation): string { - return ast.validation.map((validation) => validation.name).join(" & "); -} - -function formatExpected(ast: AST): string { - concrete(ast); - switch (ast._tag) { - case ASTTag.StringKeyword: - return getExpected(ast).getOrElse("string"); - case ASTTag.NumberKeyword: - return getExpected(ast).getOrElse("number"); - case ASTTag.BooleanKeyword: - return getExpected(ast).getOrElse("boolean"); - case ASTTag.BigIntKeyword: - return getExpected(ast).getOrElse("bigint"); - case ASTTag.UndefinedKeyword: - return getExpected(ast).getOrElse("undefined"); - case ASTTag.SymbolKeyword: - return getExpected(ast).getOrElse("symbol"); - case ASTTag.ObjectKeyword: - return getExpected(ast).getOrElse("object"); - case ASTTag.AnyKeyword: - return getExpected(ast).getOrElse("any"); - case ASTTag.UnknownKeyword: - return getExpected(ast).getOrElse("unknown"); - case ASTTag.VoidKeyword: - return getExpected(ast).getOrElse("void"); - case ASTTag.NeverKeyword: - return getExpected(ast).getOrElse("never"); - case ASTTag.Literal: - return getExpected(ast).getOrElse(formatActual(ast.literal)); - case ASTTag.UniqueSymbol: - return getExpected(ast).getOrElse(formatActual(ast.symbol)); - case ASTTag.Union: - return ast.types.map(formatExpected).join(" or "); - case ASTTag.Refinement: - return getExpected(ast).getOrElse("refinement"); - case ASTTag.TemplateLiteral: - return getExpected(ast).getOrElse(formatTemplateLiteral(ast)); - case ASTTag.Tuple: - return getExpected(ast).getOrElse("tuple or array"); - case ASTTag.TypeLiteral: - return getExpected(ast).getOrElse("type literal"); - case ASTTag.Enum: - return getExpected(ast).getOrElse(ast.enums.map(([_, value]) => JSON.stringify(value)).join(" | ")); - case ASTTag.Lazy: - return getExpected(ast).getOrElse(""); - case ASTTag.Declaration: - return getExpected(ast).getOrElse(""); - case ASTTag.Transform: - return `a parsable value from ${formatExpected(ast.from)} to ${formatExpected(ast.to)}`; - case ASTTag.Validation: - return getExpected(ast).match( - () => getMissedBrands(ast), - (expected) => `${expected} with validation(s) ${getMissedBrands(ast)}`, - ); - } -} - -function go(error: ParseError): RoseTree { - switch (error._tag) { - case ParseErrorTag.Type: - return RoseTree( - error.expected.annotations - .get(ASTAnnotation.Message) - .map((f) => f(error.actual)) - .getOrElse(`Expected ${formatExpected(error.expected)}, actual ${formatActual(error.actual)}`), - ); - case ParseErrorTag.Index: - return RoseTree(`index ${error.index}`, error.errors.map(go)); - case ParseErrorTag.Unexpected: - return RoseTree("is unexpected"); - case ParseErrorTag.Key: - return RoseTree(`key ${formatActual(error.key)}`, error.errors.map(go)); - case ParseErrorTag.Missing: - return RoseTree("is missing"); - case ParseErrorTag.UnionMember: - return RoseTree("union member", error.errors.map(go)); - case ParseErrorTag.Refinement: - return RoseTree(formatRefinementKind(error), error.errors.map(go)); - case ParseErrorTag.Transformation: - return RoseTree(formatTransformationKind(error), error.errors.map(go)); - } -} +/* eslint-disable simple-import-sort/exports */ +// codegen:start { preset: barrel, include: ./ParseError/*.ts } +export * from "./ParseError/TreeFormatter.js"; +export * from "./ParseError/PathFormatter.js"; +export * from "./ParseError/ParseErrorFormatter.js"; +export * from "./ParseError/ParseError.js"; +// codegen:end diff --git a/packages/schema/src/ParseError/ParseError.ts b/packages/schema/src/ParseError/ParseError.ts new file mode 100644 index 00000000..b0d7b1cf --- /dev/null +++ b/packages/schema/src/ParseError/ParseError.ts @@ -0,0 +1,304 @@ +import type { Declaration, Refinement, Transform, Tuple, TypeLiteral, Union } from "@fncts/schema/AST"; + +export const enum ParseErrorTag { + Declaration, + Type, + Index, + Key, + Missing, + Unexpected, + UnionMember, + Refinement, + Transformation, + TypeLiteral, + Tuple, + Union, + Iterable, +} + +/** + * @tsplus type fncts.schema.ParseError + * @tsplus companion fncts.schema.ParseErrorOps + */ +export type ParseError = + | DeclarationError + | TypeError + | RefinementError + | TransformationError + | TypeLiteralError + | TupleError + | UnionError + | IterableError; + +/** + * @tsplus companion fncts.schema.ParseError.DeclarationError + */ +export class DeclarationError { + readonly _tag = ParseErrorTag.Declaration; + constructor( + readonly ast: Declaration, + readonly actual: unknown, + readonly error: ParseError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.DeclarationError __call + * @tsplus static fncts.schema.ParseErrorOps DeclarationError + */ +export function declarationError(ast: Declaration, actual: unknown, error: ParseError): DeclarationError { + return new DeclarationError(ast, actual, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.TypeError + */ +export class TypeError { + readonly _tag = ParseErrorTag.Type; + constructor( + readonly ast: AST, + readonly actual: unknown, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.TypeError __call + * @tsplus static fncts.schema.ParseErrorOps TypeError + */ +export function typeError(expected: AST, actual: unknown): TypeError { + return new TypeError(expected, actual); +} + +/** + * @tsplus companion fncts.schema.ParseError.TypeLiteralError + */ +export class TypeLiteralError { + readonly _tag = ParseErrorTag.TypeLiteral; + constructor( + readonly ast: TypeLiteral, + readonly actual: unknown, + readonly errors: Vector, + readonly output: { readonly [x: string]: unknown } = {}, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.TypeLiteralError __call + * @tsplus static fncts.schema.ParseErrorOps TypeLiteralError + */ +export function typeLiteralError( + ast: TypeLiteral, + actual: unknown, + errors: Vector, + output: { readonly [x: string]: unknown } = {}, +): TypeLiteralError { + return new TypeLiteralError(ast, actual, errors, output); +} + +/** + * @tsplus companion fncts.schema.ParseError.TupleError + */ +export class TupleError { + readonly _tag = ParseErrorTag.Tuple; + constructor( + readonly ast: Tuple, + readonly actual: unknown, + readonly errors: Vector, + readonly output: ReadonlyArray = [], + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.TupleError __call + * @tsplus static fncts.schema.ParseErrorOps TupleError + */ +export function tupleError( + ast: Tuple, + actual: unknown, + errors: Vector, + output: ReadonlyArray = [], +): TupleError { + return new TupleError(ast, actual, errors, output); +} + +/** + * @tsplus companion fncts.schema.ParseError.IndexError + */ +export class IndexError { + readonly _tag = ParseErrorTag.Index; + constructor( + readonly index: number, + readonly error: ParseError | MissingError | UnexpectedError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.IndexError __call + * @tsplus static fncts.schema.ParseErrorOps IndexError + */ +export function indexError(index: number, error: ParseError | MissingError | UnexpectedError): IndexError { + return new IndexError(index, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.KeyError + */ +export class KeyError { + readonly _tag = ParseErrorTag.Key; + constructor( + readonly keyAST: AST, + readonly key: any, + readonly error: ParseError | MissingError | UnexpectedError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.KeyError __call + * @tsplus static fncts.schema.ParseErrorOps KeyError + */ +export function keyError(keyAST: AST, key: any, error: ParseError | MissingError | UnexpectedError): KeyError { + return new KeyError(keyAST, key, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.MissingError + */ +export class MissingError { + readonly _tag = ParseErrorTag.Missing; +} + +/** + * @tsplus static fncts.schema.ParseErrorOps MissingError + */ +export const missingError = new MissingError(); + +/** + * @tsplus companion fncts.schema.ParseError.UnexpectedError + */ +export class UnexpectedError { + readonly _tag = ParseErrorTag.Unexpected; + constructor(readonly actual: unknown) {} +} + +/** + * @tsplus static fncts.schema.ParseError.UnexpectedError __call + * @tsplus static fncts.schema.ParseErrorOps UnexpectedError + */ +export function unexpectedError(actual: unknown): UnexpectedError { + return new UnexpectedError(actual); +} + +/** + * @tsplus companion fncts.schema.ParseError.UnionError + */ +export class UnionError { + readonly _tag = ParseErrorTag.Union; + constructor( + readonly ast: Union, + readonly actual: unknown, + readonly errors: Vector, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.UnionError __call + * @tsplus static fncts.schema.ParseErrorOps UnionError + */ +export function unionError( + ast: Union, + actual: unknown, + errors: Vector, +): UnionError { + return new UnionError(ast, actual, errors); +} + +/** + * @tsplus companion fncts.schema.ParseError.UnionMemberError + */ +export class UnionMemberError { + readonly _tag = ParseErrorTag.UnionMember; + constructor( + readonly ast: AST, + readonly error: ParseError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.UnionMemberError __call + * @tsplus static fncts.schema.ParseErrorOps UnionMemberError + */ +export function unionMemberError(ast: AST, error: ParseError): UnionMemberError { + return new UnionMemberError(ast, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.RefinementError + */ +export class RefinementError { + readonly _tag = ParseErrorTag.Refinement; + constructor( + readonly ast: Refinement, + readonly actual: unknown, + readonly kind: "From" | "Predicate", + readonly error: ParseError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.RefinementError __call + * @tsplus static fncts.schema.ParseErrorOps RefinementError + */ +export function refinementError( + ast: Refinement, + actual: unknown, + kind: "From" | "Predicate", + error: ParseError, +): ParseError { + return new RefinementError(ast, actual, kind, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.TransformationError + */ +export class TransformationError { + readonly _tag = ParseErrorTag.Transformation; + constructor( + readonly ast: Transform, + readonly actual: unknown, + readonly kind: "Encoded" | "Transformation" | "Type", + readonly error: ParseError, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.TransformationError __call + * @tsplus static fncts.schema.ParseErrorOps TransformationError + */ +export function transformationError( + ast: Transform, + actual: unknown, + kind: "Encoded" | "Transformation" | "Type", + error: ParseError, +): ParseError { + return new TransformationError(ast, actual, kind, error); +} + +/** + * @tsplus companion fncts.schema.ParseError.IterableError + */ +export class IterableError { + readonly _tag = ParseErrorTag.Iterable; + constructor( + readonly ast: AST, + readonly actual: unknown, + readonly errors: Vector, + ) {} +} + +/** + * @tsplus static fncts.schema.ParseError.IterableError __call + * @tsplus static fncts.schema.ParseErrorOps IterableError + */ +export function iterableError(ast: AST, actual: unknown, errors: Vector): IterableError { + return new IterableError(ast, actual, errors); +} diff --git a/packages/schema/src/ParseError/ParseErrorFormatter.ts b/packages/schema/src/ParseError/ParseErrorFormatter.ts new file mode 100644 index 00000000..9ad01dfc --- /dev/null +++ b/packages/schema/src/ParseError/ParseErrorFormatter.ts @@ -0,0 +1 @@ +export type ParseErrorFormatter = (error: ParseError) => string; diff --git a/packages/schema/src/ParseError/PathFormatter.ts b/packages/schema/src/ParseError/PathFormatter.ts new file mode 100644 index 00000000..52ccc0d3 --- /dev/null +++ b/packages/schema/src/ParseError/PathFormatter.ts @@ -0,0 +1,117 @@ +import type { MissingError, UnexpectedError } from "./ParseError"; + +import { ParseErrorTag } from "./ParseError.js"; +import { formatTypeError, getMessage } from "./TreeFormatter.js"; + +export interface ParseErrorFlat { + type: ParseErrorTag; + path: Array; + message: string; +} + +/** + * @tsplus getter fncts.schema.ParseError flatten + */ +export function flatten(error: ParseError): Array { + return go(error, []); + + function go(error: ParseError | MissingError | UnexpectedError, path: Array): Array { + switch (error._tag) { + case ParseErrorTag.Tuple: { + return getFlattenedParseError( + error, + path, + error.errors.flatMap((error) => go(error.error, path.concat(error.index))).toArray, + ); + } + case ParseErrorTag.Union: { + return getFlattenedParseError( + error, + path, + + error.errors.flatMap((error) => { + if (error._tag === ParseErrorTag.UnionMember) { + return go(error.error, path); + } else { + return go(error, path); + } + }).toArray, + ); + } + case ParseErrorTag.TypeLiteral: { + return getFlattenedParseError( + error, + path, + error.errors.flatMap((key) => go(key.error, path.concat(key.key))).toArray, + ); + } + case ParseErrorTag.Missing: { + return [ + { + type: error._tag, + path: path, + message: "is missing", + } as const, + ]; + } + case ParseErrorTag.Unexpected: { + return [ + { + type: error._tag, + path: path, + message: "is unexpected", + } as const, + ]; + } + case ParseErrorTag.Type: { + return [ + { + type: error._tag, + path: path, + message: formatTypeError(error), + } as const, + ]; + } + case ParseErrorTag.Declaration: + case ParseErrorTag.Transformation: + case ParseErrorTag.Refinement: { + return getFlattenedParseError(error, path, go(error.error, path)); + } + case ParseErrorTag.Iterable: { + return getFlattenedParseError( + error, + path, + error.errors.flatMap((error) => { + switch (error._tag) { + case ParseErrorTag.Key: { + return go(error.error, path.concat(error.key)); + } + case ParseErrorTag.Index: { + return go(error.error, path.concat(error.index)); + } + } + }).toArray, + ); + } + } + } +} + +function getFlattenedParseError( + error: ParseError, + path: Array, + orElse: Lazy>, +): Array { + return getMessage(error).match( + () => orElse(), + (message) => { + return [ + { + type: error._tag, + path, + message, + }, + ]; + }, + ); +} diff --git a/packages/schema/src/ParseError/TreeFormatter.ts b/packages/schema/src/ParseError/TreeFormatter.ts new file mode 100644 index 00000000..4c684c49 --- /dev/null +++ b/packages/schema/src/ParseError/TreeFormatter.ts @@ -0,0 +1,127 @@ +import type { + IndexError, + KeyError, + MissingError, + RefinementError, + TransformationError, + TypeError, + UnexpectedError, +} from "./ParseError"; + +import { showWithOptions } from "@fncts/base/data/Showable"; + +import { ParseErrorTag } from "./ParseError.js"; + +/** + * @tsplus static fncts.schema.ParseErrorOps drawTree + * @tsplus getter fncts.schema.ParseError drawTree + */ +export function format(error: ParseError): string { + return go(error).draw; +} + +function formatActual(actual: unknown): string { + return showWithOptions(actual, { colors: false }); +} + +function formatRefinementKind(error: RefinementError): string { + switch (error.kind) { + case "From": { + return "From side refinement failure"; + } + case "Predicate": { + return "Predicate refinement failure"; + } + } +} + +function formatTransformationKind(error: TransformationError): string { + switch (error.kind) { + case "Encoded": { + return "Encoded side transformation failure"; + } + case "Transformation": { + return "Transformation process failure"; + } + case "Type": { + return "Type side transformation failure"; + } + } +} + +export function getMessage(error: ParseError): Maybe { + return error.ast.annotations.get(ASTAnnotation.Message).map((f) => f(error)); +} + +export function formatTypeError(error: TypeError): string { + return getMessage(error).getOrElse(`Expected ${error.ast.toString(true)}, actual ${formatActual(error.actual)}`); +} + +function formatKeyErrors(errors: Vector): Vector> { + return errors.map((error) => { + if (error._tag === ParseErrorTag.Key) { + return RoseTree(`[${String(error.keyAST)}]`, Vector(go(error.error))); + } else { + return RoseTree(`[${error.index}]`, Vector(go(error.error))); + } + }); +} + +function getRoseTree(error: ParseError, orElse: Lazy>) { + return getMessage(error).match( + () => orElse(), + (message) => RoseTree(message), + ); +} + +function go(error: ParseError | MissingError | UnexpectedError): RoseTree { + switch (error._tag) { + case ParseErrorTag.Type: + return RoseTree(formatTypeError(error)); + case ParseErrorTag.Declaration: + return getRoseTree(error, () => { + const shouldSkipDefaultMessage = error.error._tag === ParseErrorTag.Type && error.error.ast === error.ast; + if (shouldSkipDefaultMessage) { + return go(error.error); + } else { + return RoseTree(error.ast.toString(true), Vector(go(error.error))); + } + }); + case ParseErrorTag.Unexpected: + return RoseTree("is unexpected"); + case ParseErrorTag.Missing: + return RoseTree("is missing"); + case ParseErrorTag.TypeLiteral: + return getRoseTree(error, () => { + return RoseTree(error.ast.toString(true), formatKeyErrors(error.errors)); + }); + case ParseErrorTag.Tuple: + return getRoseTree(error, () => { + return RoseTree(error.ast.toString(true), formatKeyErrors(error.errors)); + }); + case ParseErrorTag.Union: + return RoseTree( + error.ast.toString(true), + error.errors.map((error) => { + switch (error._tag) { + case ParseErrorTag.UnionMember: + return RoseTree("Union member", Vector(go(error.error))); + default: + return go(error); + } + }), + ); + case ParseErrorTag.Refinement: + return getRoseTree(error, () => { + return RoseTree(formatRefinementKind(error), Vector(go(error.error))); + }); + case ParseErrorTag.Transformation: + return getRoseTree(error, () => { + return RoseTree(formatTransformationKind(error), Vector(go(error.error))); + }); + case ParseErrorTag.Iterable: + return getRoseTree(error, () => { + return RoseTree(error.ast.toString(true), formatKeyErrors(error.errors)); + }); + } +} diff --git a/packages/schema/src/ParseFailure.ts b/packages/schema/src/ParseFailure.ts deleted file mode 100644 index a12ee98d..00000000 --- a/packages/schema/src/ParseFailure.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const ParseFailureTypeId = Symbol.for("fncts.schema.ParseFailure"); -export type ParseFailureTypeId = typeof ParseFailureTypeId; - -/** - * @tsplus type fncts.schema.ParseFailure - * @tsplus companion fncts.schema.ParseFailureOps - */ -export class ParseFailure { - readonly [ParseFailureTypeId]: ParseFailureTypeId = ParseFailureTypeId; - constructor(readonly errors: Vector) {} -} - -/** - * @tsplus static fncts.schema.ParseFailureOps __call - */ -export function make(errors: Vector): ParseFailure { - return new ParseFailure(errors); -} diff --git a/packages/schema/src/ParseResult.ts b/packages/schema/src/ParseResult.ts index 25854323..09fd95ca 100644 --- a/packages/schema/src/ParseResult.ts +++ b/packages/schema/src/ParseResult.ts @@ -2,7 +2,7 @@ * @tsplus type fncts.schema.ParseResult * @tsplus companion fncts.schema.ParseResultOps */ -export interface ParseResult extends Either {} +export interface ParseResult extends Either {} /** * @tsplus static fncts.schema.ParseResultOps succeed @@ -11,16 +11,9 @@ export function succeed(value: A): ParseResult { return Either.right(value); } -/** - * @tsplus static fncts.schema.ParseResultOps failures - */ -export function failures(value: Vector): ParseResult { - return Either.left(ParseFailure(value)); -} - /** * @tsplus static fncts.schema.ParseResultOps fail */ export function fail(value: ParseError): ParseResult { - return Either.left(ParseFailure(Vector(value))); + return Either.left(value); } diff --git a/packages/schema/src/Parser/api.ts b/packages/schema/src/Parser/api.ts index 75415b64..83f90387 100644 --- a/packages/schema/src/Parser/api.ts +++ b/packages/schema/src/Parser/api.ts @@ -45,7 +45,7 @@ function parseOrThrow(ast: AST) { const parser = parserFor(ast, true); return (input: unknown, options?: ParseOptions) => { return parser(input, options).match((failure) => { - throw new Error(ParseError.format(failure.errors)); + throw new Error(ParseError.drawTree(failure)); }, Function.identity); }; } diff --git a/packages/schema/src/Parser/interpreter.ts b/packages/schema/src/Parser/interpreter.ts index d218842f..535a34e2 100644 --- a/packages/schema/src/Parser/interpreter.ts +++ b/packages/schema/src/Parser/interpreter.ts @@ -1,3 +1,4 @@ +import type { IndexError, KeyError, TypeError, TypeLiteralError, UnionMemberError } from "../ParseError/ParseError.js"; import type { MutableVector } from "@fncts/base/collection/immutable/Vector"; import type { Validation } from "@fncts/base/data/Branded"; @@ -14,7 +15,7 @@ import { } from "@fncts/base/util/predicates"; import { ASTTag, concrete, getSearchTree } from "../AST.js"; -import { RefinementError, TransformationError } from "../ParseError.js"; +import { DeclarationError, ParseErrorTag, RefinementError, TransformationError } from "../ParseError/ParseError.js"; import { getKeysForIndexSignature, getTemplateLiteralRegex, memoize, ownKeys } from "../utils.js"; const decodeMemoMap = globalValue( @@ -41,8 +42,13 @@ function goMemo(ast: AST, isDecoding: boolean): Parser { function go(ast: AST, isDecoding: boolean): Parser { concrete(ast); switch (ast._tag) { - case ASTTag.Declaration: - return Parser.make(ast.decode(...ast.typeParameters)); + case ASTTag.Declaration: { + const parse = isDecoding ? ast.decode(...ast.typeParameters) : ast.encode(...ast.typeParameters); + + return Parser.make((input, options) => + parse(input, options).mapLeft((error) => new DeclarationError(ast, input, error)), + ); + } case ASTTag.Literal: return Parser.fromRefinement(ast, (u): u is typeof ast.literal => u === ast.literal); case ASTTag.UniqueSymbol: @@ -81,18 +87,18 @@ function go(ast: AST, isDecoding: boolean): Parser { return ParseResult.fail(ParseError.TypeError(AST.unknownArray, input)); } const output: Array = []; - const errors: MutableVector = Vector.emptyPushable(); + const errors: MutableVector = Vector.emptyPushable(); const allErrors = options?.allErrors; let i = 0; for (; i < elements.length; i++) { if (input.length < i + 1) { if (!ast.elements[i]!.isOptional) { - const e = ParseError.IndexError(i, Vector(ParseError.MissingError)); - errors.push(e); + const e = ParseError.IndexError(i, ParseError.MissingError); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); } } } else { @@ -100,12 +106,12 @@ function go(ast: AST, isDecoding: boolean): Parser { const t = parser(input[i], options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.IndexError(i, t.left.errors); - errors.push(e); + const e = ParseError.IndexError(i, t.left); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); } } output.push(t.right); @@ -119,12 +125,12 @@ function go(ast: AST, isDecoding: boolean): Parser { const t = head(input[i], options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.IndexError(i, t.left.errors); - errors.push(e); + const e = ParseError.IndexError(i, t.left); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); } } output.push(t.right); @@ -132,18 +138,23 @@ function go(ast: AST, isDecoding: boolean): Parser { for (let j = 0; j < tail.length; j++) { i += j; if (input.length < i + 1) { - errors.push(ParseError.IndexError(i, Vector(ParseError.MissingError))); - return ParseResult.failures(errors); + const e = ParseError.IndexError(i, ParseError.MissingError); + if (allErrors) { + errors.push(e); + continue; + } else { + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); + } } else { const t = tail[j]!(input[i], options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.IndexError(i, t.left.errors); - errors.push(e); + const e = ParseError.IndexError(i, t.left); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); } } output.push(t.right); @@ -152,18 +163,20 @@ function go(ast: AST, isDecoding: boolean): Parser { } else { const isUnexpectedAllowed = options?.isUnexpectedAllowed; for (; i < input.length; i++) { - const e = ParseError.IndexError(i, Vector(ParseError.UnexpectedError(input[i]))); + const e = ParseError.IndexError(i, ParseError.UnexpectedError(input[i])); if (!isUnexpectedAllowed) { - errors.push(e); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TupleError(ast, input, Vector(e), output)); } } } } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(output); + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.TupleError(ast, input, errors, output)) + : ParseResult.succeed(output); }); } case ASTTag.TypeLiteral: { @@ -178,9 +191,9 @@ function go(ast: AST, isDecoding: boolean): Parser { if (!isRecord(input)) { return ParseResult.fail(ParseError.TypeError(AST.unknownRecord, input)); } - const output: any = {}; - const expectedKeys: any = {}; - const errors: MutableVector = Vector.emptyPushable(); + const output: any = {}; + const expectedKeys: any = {}; + const errors: MutableVector = Vector.emptyPushable(); const allErrors = options?.allErrors; for (let i = 0; i < propertySignatureTypes.length; i++) { @@ -190,24 +203,24 @@ function go(ast: AST, isDecoding: boolean): Parser { expectedKeys[name] = null; if (!Object.prototype.hasOwnProperty.call(input, name)) { if (!ps.isOptional) { - const e = ParseError.KeyError(AST.createKey(name), name, Vector(ParseError.MissingError)); - errors.push(e); + const e = ParseError.KeyError(AST.createKey(name), name, ParseError.MissingError); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TypeLiteralError(ast, input, Vector(e), output)); } } } else { const t: ParseResult = parser(input[name], options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.KeyError(AST.createKey(name), name, t.left.errors); - errors.push(e); + const e = ParseError.KeyError(AST.createKey(name), name, t.left); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TypeLiteralError(ast, input, Vector(e), output)); } } output[name] = t.right; @@ -226,24 +239,24 @@ function go(ast: AST, isDecoding: boolean): Parser { let t = parameter(key, options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.KeyError(AST.createKey(key), key, t.left.errors); - errors.push(e); + const e = ParseError.KeyError(AST.createKey(key), key, t.left); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TypeLiteralError(ast, input, Vector(e), output)); } } t = type(input[key], options); Either.concrete(t); if (t.isLeft()) { - const e = ParseError.KeyError(AST.createKey(key), key, t.left.errors); + const e = ParseError.KeyError(AST.createKey(key), key, t.left); errors.push(e); if (allErrors) { continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TypeLiteralError(ast, input, Vector(e), output)); } } @@ -254,33 +267,36 @@ function go(ast: AST, isDecoding: boolean): Parser { const isUnexpectedAllowed = options?.isUnexpectedAllowed; for (const key of ownKeys(input)) { if (!Object.prototype.hasOwnProperty.call(expectedKeys, key)) { - const e = ParseError.KeyError(AST.createKey(key), key, Vector(ParseError.UnexpectedError(input[key]))); if (!isUnexpectedAllowed) { - errors.push(e); + const e = ParseError.KeyError(AST.createKey(key), key, ParseError.UnexpectedError(input[key])); if (allErrors) { + errors.push(e); continue; } else { - return ParseResult.failures(errors); + return ParseResult.fail(ParseError.TypeLiteralError(ast, input, Vector(e), output)); } } } } } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(output); + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.TypeLiteralError(ast, input, errors, output)) + : ParseResult.succeed(output); }); } case ASTTag.Union: { const searchTree = getSearchTree(ast.types, isDecoding); const ownKeys = Reflect.ownKeys(searchTree.keys); const len = ownKeys.length; - const otherwise = searchTree.otherwise; const map = new Map>(); ast.types.forEach((ast) => { map.set(ast, goMemo(ast, isDecoding)); }); return Parser.make((input, options) => { - const errors = Vector.emptyPushable(); + const errors = Vector.emptyPushable(); + let candidates: Array = []; + if (len > 0) { if (isRecord(input)) { for (let i = 0; i < len; i++) { @@ -289,45 +305,58 @@ function go(ast: AST, isDecoding: boolean): Parser { if (Object.prototype.hasOwnProperty.call(input, name)) { const literal = String(input[name]); if (Object.prototype.hasOwnProperty.call(buckets, literal)) { - const bucket: ReadonlyArray = buckets[literal]!; - for (let i = 0; i < bucket.length; i++) { - const t = map.get(bucket[i])!(input, options); - Either.concrete(t); - if (t.isRight()) { - return t; - } else { - errors.push(ParseError.UnionMemberError(t.left.errors)); - } - } + candidates = candidates.concat(buckets[literal]!); } else { + const literals = AST.createUnion(Vector.from(searchTree.keys[name]!.literals)); errors.push( - ParseError.KeyError( - AST.createKey(name), - name, - Vector(ParseError.TypeError(searchTree.keys[name]!.ast, input[name])), + ParseError.TypeLiteralError( + AST.createTypeLiteral(Vector(AST.createPropertySignature(name, literals, false, true)), Vector()), + input, + Vector( + ParseError.KeyError( + AST.createKey(name), + name, + ParseError.TypeError(searchTree.keys[name]!.ast, input[name]), + ), + ), ), ); } } else { - errors.push(ParseError.KeyError(AST.createKey(name), name, Vector(ParseError.MissingError))); + const literals = AST.createUnion(Vector.from(searchTree.keys[name]!.literals)); + errors.push( + ParseError.TypeLiteralError( + AST.createTypeLiteral(Vector(AST.createPropertySignature(name, literals, false, true)), Vector()), + input, + Vector(ParseError.KeyError(AST.createKey(name), name, ParseError.MissingError)), + ), + ); } } } else { errors.push(ParseError.TypeError(AST.unknownRecord, input)); } } - for (let i = 0; i < otherwise.length; i++) { - const t = map.get(otherwise[i])!(input, options); - Either.concrete(t); - if (t.isRight()) { - return t; + + if (searchTree.otherwise.length > 0) { + candidates = candidates.concat(searchTree.otherwise); + } + + for (let i = 0; i < candidates.length; i++) { + const candidate = candidates[i]!; + const pr = map.get(candidate)!(input, options); + Either.concrete(pr); + if (pr.isRight()) { + return pr; } else { - errors.push(ParseError.UnionMemberError(t.left.errors)); + errors.push(ParseError.UnionMemberError(candidate, pr.left)); } } return errors.isNonEmpty() - ? ParseResult.failures(errors) + ? errors.length === 1 && errors[0]!._tag === ParseErrorTag.Type + ? ParseResult.fail(errors[0]! as TypeError) + : ParseResult.fail(ParseError.UnionError(ast, input, Vector.from(errors))) : ParseResult.fail(ParseError.TypeError(AST.neverKeyword, input)); }); } @@ -341,11 +370,9 @@ function go(ast: AST, isDecoding: boolean): Parser { const from = goMemo(ast.from, isDecoding); return Parser.make((input, options) => from(input, options) - .mapLeft((failure) => ParseFailure(Vector(RefinementError(ast, input, "From", failure.errors)))) + .mapLeft((failure) => RefinementError(ast, input, "From", failure)) .flatMap((a) => - ast - .decode(a, options) - .mapLeft((failure) => ParseFailure(Vector(RefinementError(ast, input, "Predicate", failure.errors)))), + ast.decode(a, options).mapLeft((failure) => RefinementError(ast, input, "Predicate", failure)), ), ); } else { @@ -364,17 +391,13 @@ function go(ast: AST, isDecoding: boolean): Parser { const to = isDecoding ? goMemo(ast.to, true) : goMemo(ast.from, false); return Parser.make((input, options) => from(input, options) - .mapLeft((failure) => - ParseFailure(Vector(TransformationError(ast, input, isDecoding ? "Encoded" : "Type", failure.errors))), - ) + .mapLeft((failure) => TransformationError(ast, input, isDecoding ? "Encoded" : "Type", failure)) .flatMap((a) => - transformation(a, options).mapLeft((failure) => - ParseFailure(Vector(TransformationError(ast, input, "Transformation", failure.errors))), - ), + transformation(a, options).mapLeft((failure) => TransformationError(ast, input, "Transformation", failure)), ) .flatMap((a) => to(a, options).mapLeft((failure) => - ParseFailure(Vector(TransformationError(ast, input, isDecoding ? "Type" : "Encoded", failure.errors))), + TransformationError(ast, input, isDecoding ? "Type" : "Encoded", failure), ), ), ); diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index fd628add..93c43882 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -7,7 +7,9 @@ export * from "./Schema/api.js"; /* eslint-disable simple-import-sort/exports */ // codegen:start { preset: barrel, include: ./Schema/api/*.ts } +export * from "./Schema/api/set.js"; export * from "./Schema/api/maybe.js"; +export * from "./Schema/api/map.js"; export * from "./Schema/api/list.js"; export * from "./Schema/api/immutableArray.js"; export * from "./Schema/api/hashSet.js"; diff --git a/packages/schema/src/Schema/api.ts b/packages/schema/src/Schema/api.ts index 19d36c0f..d5d67fea 100644 --- a/packages/schema/src/Schema/api.ts +++ b/packages/schema/src/Schema/api.ts @@ -28,15 +28,15 @@ export function annotate(annotation: ASTAnnotation, value: V) { */ export function declaration( typeParameters: Vector>, - type: Schema, decode: (...typeParameters: ReadonlyArray>) => Parser, + encode: (...typeParameters: ReadonlyArray>) => Parser, annotations?: ASTAnnotationMap, ): Schema { return Schema.fromAST( AST.createDeclaration( typeParameters.map((tp) => tp.ast), - type.ast, (...typeParameters) => decode(...typeParameters.map(Schema.fromAST)), + (...typeParameters) => encode(...typeParameters.map(Schema.fromAST)), annotations, ), ); @@ -541,3 +541,21 @@ export function transform( ); }; } + +/** + * @tsplus pipeable fncts.schema.Schema pick + */ +export function pick>(...keys: Keys) { + return (self: Schema): Schema> => { + return Schema.fromAST(self.ast.pick(Vector.from(keys))); + }; +} + +/** + * @tsplus pipeable fncts.schema.Schema omit + */ +export function omit>(...keys: Keys) { + return (self: Schema): Schema> => { + return Schema.fromAST(self.ast.omit(Vector.from(keys))); + }; +} diff --git a/packages/schema/src/Schema/api/conc.ts b/packages/schema/src/Schema/api/conc.ts index b4dbf38d..3a3d0b8e 100644 --- a/packages/schema/src/Schema/api/conc.ts +++ b/packages/schema/src/Schema/api/conc.ts @@ -1,14 +1,10 @@ +import type { IndexError } from "@fncts/schema/ParseError"; import type { Sized } from "@fncts/test/control/Sized"; -import { ConcTypeId } from "@fncts/base/collection/immutable/Conc"; - export function conc(value: Schema): Schema> { - return Schema.declaration( - Vector(value), - inline(value), - concParser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "Conc").annotate(ASTAnnotation.GenHook, gen), - ); + return Schema.declaration(Vector(value), concParser(true), concParser(false)) + .annotate(ASTAnnotation.Identifier, `Conc<${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); } /** @@ -35,42 +31,37 @@ export function deriveConc>( return unsafeCoerce(concFromArray(value)); } -export function concParser(value: Schema): Parser> { - const schema = conc(value); - return Parser.make((u, options) => { - if (!Conc.is(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - const allErrors = options?.allErrors; - const errors = Vector.emptyPushable(); - const out: Array = []; - let i = 0; - for (const v of u) { - const t = value.decode(v); - Either.concrete(t); - if (t.isLeft()) { - errors.push(ParseError.IndexError(i, t.left.errors)); - if (!allErrors) { - return ParseResult.failures(errors); - } - } else { - out.push(t.right); +function concParser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = conc(value); + return Parser.make((u, options) => { + if (!Conc.is(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); } - i++; - } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(Conc.fromArray(out)); - }); -} + const allErrors = options?.allErrors; + const errors = Vector.emptyPushable(); + const out: Array = []; + let i = 0; -function inline(_value: Schema): Schema> { - return Schema.struct({ - _A: Schema.any, - [ConcTypeId]: Schema.uniqueSymbol(ConcTypeId), - length: Schema.number, - [Symbol.iterator]: Schema.any, - [Symbol.hash]: Schema.any, - [Symbol.equals]: Schema.any, - }); + for (const v of u) { + const parser = isDecoding ? value.decode : value.encode; + const t = parser(v, options); + Either.concrete(t); + if (t.isLeft()) { + errors.push(ParseError.IndexError(i, t.left)); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + } else { + out.push(t.right); + } + i++; + } + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(Conc.from(out)); + }); + }; } function gen(value: Gen): Gen> { diff --git a/packages/schema/src/Schema/api/either.ts b/packages/schema/src/Schema/api/either.ts index 26e8bdf4..e37eadec 100644 --- a/packages/schema/src/Schema/api/either.ts +++ b/packages/schema/src/Schema/api/either.ts @@ -1,17 +1,12 @@ import type { EitherJson } from "@fncts/base/json/EitherJson"; -import { EitherTypeId, EitherVariance } from "@fncts/base/data/Either"; -import { IOTypeId } from "@fncts/io/IO"; - /** * @tsplus static fncts.schema.SchemaOps either */ export function either(left: Schema, right: Schema): Schema> { - return Schema.declaration( - Vector(left, right), - eitherInline(left, right), - eitherParser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "Either"), + return Schema.declaration(Vector(left, right), eitherParser(true), eitherParser(false)).annotate( + ASTAnnotation.Identifier, + `Either<${left.show()}, ${right.show()}>`, ); } @@ -71,26 +66,21 @@ export function deriveEither>( return unsafeCoerce(eitherFromJson(left, right)); } -function eitherParser(left: Schema, right: Schema): Parser> { - const schema = either(left, right); - return Parser.make((u, options) => { - if (!Either.isEither(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - Either.concrete(u); - if (u.isLeft()) { - return left.decode(u.left, options).map(Either.left); - } else { - return right.decode(u.right, options).map(Either.right); - } - }); -} - -function eitherInline(_left: Schema, _right: Schema): Schema> { - return Schema.struct({ - [EitherTypeId]: Schema.uniqueSymbol(EitherTypeId), - [EitherVariance]: Schema.any, - [IOTypeId]: Schema.uniqueSymbol(IOTypeId), - trace: Schema.undefined, - }) as unknown as Schema>; +function eitherParser(isDecoding: boolean) { + return (left: Schema, right: Schema): Parser> => { + const schema = either(left, right); + return Parser.make((u, options) => { + if (!Either.isEither(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); + } + Either.concrete(u); + if (u.isLeft()) { + const parse = isDecoding ? left.decode : left.encode; + return parse(u.left, options).map(Either.left); + } else { + const parse = isDecoding ? right.decode : right.encode; + return parse(u.right, options).map(Either.right); + } + }); + }; } diff --git a/packages/schema/src/Schema/api/hashMap.ts b/packages/schema/src/Schema/api/hashMap.ts index ef1ec120..9bedc763 100644 --- a/packages/schema/src/Schema/api/hashMap.ts +++ b/packages/schema/src/Schema/api/hashMap.ts @@ -1,28 +1,17 @@ -import type { - ArrayNode, - CollisionNode, - EmptyNode, - IndexedNode, - LeafNode, - Node, -} from "@fncts/base/collection/immutable/HashMap/internal"; +import type { KeyError } from "@fncts/schema/ParseError"; import type { Sized } from "@fncts/test/control/Sized"; import type { Check } from "@fncts/typelevel"; import { HashMap } from "@fncts/base/collection/immutable/HashMap"; -import { HashMapTypeId, HashMapVariance } from "@fncts/base/collection/immutable/HashMap"; import { ASTAnnotation } from "@fncts/schema/ASTAnnotation"; /** * @tsplus static fncts.schema.SchemaOps hashMap */ export function hashMap(key: Schema, value: Schema): Schema> { - return Schema.declaration( - Vector(key, value), - inline(key, value), - hashMapParser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "HashMap").annotate(ASTAnnotation.GenHook, gen), - ); + return Schema.declaration(Vector(key, value), hashMapParser(true), hashMapParser(false)) + .annotate(ASTAnnotation.Identifier, `HashMap<${key.show()}, ${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); } /** @@ -65,118 +54,45 @@ export function deriveHashMap>( return unsafeCoerce(hashMapFromRecord(key as Schema, value)); } -function hashMapParser(key: Schema, value: Schema): Parser> { - const schema = hashMap(key, value); - return Parser.make((u, options) => { - if (!HashMap.is(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - const allErrors = options?.allErrors; - const errors = Vector.emptyPushable(); - const out = HashMap.empty().beginMutation; - for (const [k, v] of u) { - const tk = key.decode(k, options); - Either.concrete(tk); - if (tk.isLeft()) { - errors.push(ParseError.KeyError(key.ast, k, tk.left.errors)); - if (!allErrors) { - return ParseResult.failures(errors); - } +function hashMapParser(isDecoding: boolean) { + return (key: Schema, value: Schema): Parser> => { + const schema = hashMap(key, value); + return Parser.make((u, options) => { + if (!HashMap.is(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); } - const tv = value.decode(v, options); - Either.concrete(tv); - if (tv.isLeft()) { - errors.push(ParseError.TypeError(value.ast, tv.left)); - if (!allErrors) { - return ParseResult.failures(errors); + const allErrors = options?.allErrors; + const errors = Vector.emptyPushable(); + const out = HashMap.empty().beginMutation; + const keyParser = isDecoding ? key.decode : key.encode; + const valueParser = isDecoding ? value.decode : value.encode; + for (const [k, v] of u) { + const tk = keyParser(k, options); + Either.concrete(tk); + if (tk.isLeft()) { + errors.push(ParseError.KeyError(key.ast, k, tk.left)); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } } + const tv = valueParser(v, options); + Either.concrete(tv); + if (tv.isLeft()) { + errors.push(ParseError.KeyError(key.ast, k, ParseError.TypeError(value.ast, tv.left))); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + } + if (tk.isLeft() || tv.isLeft()) { + continue; + } + out.set(tk.right, tv.right); } - if (tk.isLeft() || tv.isLeft()) { - continue; - } - out.set(tk.right, tv.right); - } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(out.endMutation); - }); -} - -function emptyNodeSchema(_key: Schema, _value: Schema): Schema> { - return Schema.struct({ - _tag: Schema.literal("EmptyNode"), - modify: Schema.any, - }); -} - -function leafNodeSchema(key: Schema, value: Schema): Schema> { - return Schema.struct({ - _tag: Schema.literal("LeafNode"), - edit: Schema.number, - hash: Schema.number, - key, - value: value.maybe, - modify: Schema.any, - }); -} - -function collisionNodeSchema(key: Schema, value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("CollisionNode"), - edit: Schema.number, - hash: Schema.number, - children: nodeSchema(key, value).mutableArray, - modify: Schema.any, - }), - ); -} - -function indexedNodeSchema(key: Schema, value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("IndexedNode"), - edit: Schema.number, - mask: Schema.number, - children: nodeSchema(key, value).mutableArray, - modify: Schema.any, - }), - ); -} - -function arrayNodeSchema(key: Schema, value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("ArrayNode"), - edit: Schema.number, - size: Schema.number, - children: nodeSchema(key, value).mutableArray, - modify: Schema.any, - }), - ); -} - -function nodeSchema(key: Schema, value: Schema): Schema> { - return Schema.union( - emptyNodeSchema(key, value), - leafNodeSchema(key, value), - collisionNodeSchema(key, value), - indexedNodeSchema(key, value), - arrayNodeSchema(key, value), - ); -} - -function inline(key: Schema, value: Schema): Schema> { - return Schema.struct({ - [HashMapTypeId]: Schema.uniqueSymbol(HashMapTypeId), - [HashMapVariance]: Schema.any, - editable: Schema.boolean, - edit: Schema.number, - config: Schema.any, - root: nodeSchema(key, value), - size: Schema.number, - [Symbol.iterator]: Schema.any, - [Symbol.hash]: Schema.any, - [Symbol.equals]: Schema.any, - }); + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(out.endMutation); + }); + }; } function gen(key: Gen, value: Gen): Gen> { diff --git a/packages/schema/src/Schema/api/hashSet.ts b/packages/schema/src/Schema/api/hashSet.ts index d45aa727..c8a5fe4f 100644 --- a/packages/schema/src/Schema/api/hashSet.ts +++ b/packages/schema/src/Schema/api/hashSet.ts @@ -1,25 +1,12 @@ +import type { KeyError } from "@fncts/schema/ParseError"; import type { Sized } from "@fncts/test/control/Sized"; -import { - type ArrayNode, - type CollisionNode, - type EmptyNode, - HashSetTypeId, - HashSetVariance, - type IndexedNode, - type LeafNode, - type Node, -} from "@fncts/base/collection/immutable/HashSet/definition"; import { ASTAnnotation } from "@fncts/schema/ASTAnnotation"; -import { ASTAnnotationMap } from "@fncts/schema/ASTAnnotationMap"; export function hashSet(value: Schema): Schema> { - return Schema.declaration( - Vector(value), - inline(value), - hashSetParser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "HashMap").annotate(ASTAnnotation.GenHook, gen), - ); + return Schema.declaration(Vector(value), hashSetParser(true), hashSetParser(false)) + .annotate(ASTAnnotation.Identifier, `HashSet<${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); } /** @@ -58,108 +45,35 @@ export function deriveHashSet>( return unsafeCoerce(hashSetFromArray(value)); } -function hashSetParser(value: Schema): Parser> { - const schema = hashSet(value); - return Parser.make((u, options) => { - if (!HashSet.is(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - const allErrors = options?.allErrors; - const errors = Vector.emptyPushable(); - const out = HashSet.empty().beginMutation; - for (const v of u) { - const tv = value.decode(v, options); - Either.concrete(tv); - if (tv.isLeft()) { - errors.push(ParseError.TypeError(value.ast, tv.left)); - if (!allErrors) { - return ParseResult.failures(errors); - } - continue; +function hashSetParser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = hashSet(value); + const parseValue = isDecoding ? value.decode : value.encode; + return Parser.make((u, options) => { + if (!HashSet.is(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); } - out.add(tv.right); - } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(out.endMutation); - }); -} -function emptyNodeSchema(_value: Schema): Schema> { - return Schema.struct({ - _tag: Schema.literal("EmptyNode"), - modify: Schema.any, - }); -} - -function leafNodeSchema(value: Schema): Schema> { - return Schema.struct({ - _tag: Schema.literal("LeafNode"), - edit: Schema.number, - hash: Schema.number, - value: value, - modify: Schema.any, - }); -} - -function collisionNodeSchema(value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("CollisionNode"), - edit: Schema.number, - hash: Schema.number, - children: nodeSchema(value).mutableArray, - modify: Schema.any, - }), - ); -} - -function indexedNodeSchema(value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("IndexedNode"), - edit: Schema.number, - mask: Schema.number, - children: nodeSchema(value).mutableArray, - modify: Schema.any, - }), - ); -} - -function arrayNodeSchema(value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("ArrayNode"), - edit: Schema.number, - size: Schema.number, - children: nodeSchema(value).mutableArray, - modify: Schema.any, - }), - ); -} - -function nodeSchema(value: Schema): Schema> { - return Schema.union( - emptyNodeSchema(value), - leafNodeSchema(value), - collisionNodeSchema(value), - indexedNodeSchema(value), - arrayNodeSchema(value), - ); -} - -function inline(value: Schema): Schema> { - return Schema.struct({ - [HashSetTypeId]: Schema.uniqueSymbol(HashSetTypeId), - [HashSetVariance]: Schema.any, - _editable: Schema.boolean, - _edit: Schema.number, - config: Schema.any, - _root: nodeSchema(value), - _size: Schema.number, - size: Schema.number, - [Symbol.iterator]: Schema.any, - [Symbol.hash]: Schema.any, - [Symbol.equals]: Schema.any, - }); + const allErrors = options?.allErrors; + const errors = Vector.emptyPushable(); + const out = HashSet.empty().beginMutation; + for (const v of u) { + const tv = parseValue(v, options); + Either.concrete(tv); + if (tv.isLeft()) { + errors.push(ParseError.KeyError(value.ast, value, tv.left)); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + continue; + } + out.add(tv.right); + } + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(out.endMutation); + }); + }; } function gen(value: Gen): Gen> { diff --git a/packages/schema/src/Schema/api/immutableArray.ts b/packages/schema/src/Schema/api/immutableArray.ts index 8887c973..72a1f85c 100644 --- a/packages/schema/src/Schema/api/immutableArray.ts +++ b/packages/schema/src/Schema/api/immutableArray.ts @@ -1,10 +1,6 @@ import type { Sized } from "@fncts/test/control/Sized"; -import { - ImmutableArray, - ImmutableArrayTypeId, - ImmutableArrayVariance, -} from "@fncts/base/collection/immutable/ImmutableArray"; +import { ImmutableArray } from "@fncts/base/collection/immutable/ImmutableArray"; import { Vector } from "@fncts/base/collection/immutable/Vector"; /** @@ -12,12 +8,9 @@ import { Vector } from "@fncts/base/collection/immutable/Vector"; * @tsplus static fncts.schema.SchemaOps immutableArray */ export function immutableArray(value: Schema): Schema> { - return Schema.declaration( - Vector(value), - inline(value), - parser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "ImmutableArray").annotate(ASTAnnotation.GenHook, gen), - ); + return Schema.declaration(Vector(value), parser(true), parser(false)) + .annotate(ASTAnnotation.Identifier, `ImmutableArray<${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); } /** @@ -45,42 +38,19 @@ export function deriveImmutableArray>( return unsafeCoerce(immutableArrayFromArray(value)); } -function parser(value: Schema): Parser> { - const schema = immutableArray(value); - return Parser.make((u, options) => { - if (!ImmutableArray.is(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - const out: Array = []; - const errors = Vector.emptyPushable(); - const allErrors = options?.allErrors; - const index = 0; - for (const v of u) { - const t = value.decode(v, options); - Either.concrete(t); - if (t.isLeft()) { - errors.push(ParseError.IndexError(index, t.left.errors)); - if (allErrors) { - continue; - } - return ParseResult.failures(errors); - } else { - out.push(t.right); +function parser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = immutableArray(value); + const arraySchema = value.array; + const parse = isDecoding ? arraySchema.decode : arraySchema.encode; + return Parser.make((u, options) => { + if (!ImmutableArray.is(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); } - } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(new ImmutableArray(out)); - }); -} -function inline(value: Schema): Schema> { - return Schema.struct({ - [ImmutableArrayTypeId]: Schema.uniqueSymbol(ImmutableArrayTypeId), - [ImmutableArrayVariance]: Schema.any, - _array: Schema.array(value), - [Symbol.equals]: Schema.any, - [Symbol.hash]: Schema.any, - [Symbol.iterator]: Schema.any, - }); + return parse(u, options).map((out) => new ImmutableArray(out as Array)); + }); + }; } function gen(value: Gen): Gen> { diff --git a/packages/schema/src/Schema/api/list.ts b/packages/schema/src/Schema/api/list.ts index b07cc0b7..e3487422 100644 --- a/packages/schema/src/Schema/api/list.ts +++ b/packages/schema/src/Schema/api/list.ts @@ -1,18 +1,14 @@ +import type { IndexError } from "@fncts/schema/ParseError"; import type { Sized } from "@fncts/test/control/Sized"; -import { ListTypeId } from "@fncts/base/collection/immutable/List"; - /** * @tsplus static fncts.schema.SchemaOps list * @tsplus getter fncts.Schema.Schema list */ export function list(value: Schema): Schema> { - return Schema.declaration( - Vector(value), - inline(value), - parser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "List").annotate(ASTAnnotation.GenHook, gen), - ); + return Schema.declaration(Vector(value), parser(true), parser(false)) + .annotate(ASTAnnotation.Identifier, `List<${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); } /** @@ -40,55 +36,36 @@ export function deriveList>( return unsafeCoerce(listFromArray(value)); } -function parser(value: Schema): Parser> { - const schema = list(value); - return Parser.make((u, options) => { - if (!List.is(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - const out = new ListBuffer(); - const errors = Vector.emptyPushable(); - const allErrors = options?.allErrors; - const index = 0; - for (const v of u) { - const t = value.decode(v, options); - Either.concrete(t); - if (t.isLeft()) { - errors.push(ParseError.IndexError(index, t.left.errors)); - if (allErrors) { - continue; +function parser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = list(value); + const parseValue = isDecoding ? value.decode : value.encode; + return Parser.make((u, options) => { + if (!List.is(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); + } + const out = new ListBuffer(); + const errors = Vector.emptyPushable(); + const allErrors = options?.allErrors; + const index = 0; + for (const v of u) { + const t = parseValue(v, options); + Either.concrete(t); + if (t.isLeft()) { + errors.push(ParseError.IndexError(index, t.left)); + if (allErrors) { + continue; + } + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } else { + out.append(t.right); } - return ParseResult.failures(errors); - } else { - out.append(t.right); } - } - return errors.isNonEmpty() ? ParseResult.failures(errors) : ParseResult.succeed(out.toList); - }); -} - -function nil(_value: Schema): Schema> { - return Schema.struct({ - _tag: Schema.literal("Nil"), - [ListTypeId]: Schema.uniqueSymbol(ListTypeId), - [Symbol.iterator]: Schema.any, - }); -} - -function cons(value: Schema): Schema> { - return Schema.lazy(() => - Schema.struct({ - _tag: Schema.literal("Cons"), - [ListTypeId]: Schema.uniqueSymbol(ListTypeId), - [Symbol.iterator]: Schema.any, - head: value, - tail: inline(value), - }), - ); -} - -function inline(value: Schema): Schema> { - return Schema.lazy(() => Schema.union(nil(value), cons(value))); + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(out.toList); + }); + }; } function gen(value: Gen): Gen> { diff --git a/packages/schema/src/Schema/api/map.ts b/packages/schema/src/Schema/api/map.ts new file mode 100644 index 00000000..bef7205d --- /dev/null +++ b/packages/schema/src/Schema/api/map.ts @@ -0,0 +1,93 @@ +import type { KeyError } from "@fncts/schema/ParseError"; +import type { Sized } from "@fncts/test/control/Sized"; + +/** + * @tsplus static fncts.schema.SchemaOps map + */ +export function map(key: Schema, value: Schema): Schema> { + return Schema.declaration(Vector(key, value), mapParser(true), mapParser(false)) + .annotate(ASTAnnotation.Identifier, `Map<${key.show()}, ${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); +} + +/** + * @tsplus static fncts.schema.SchemaOps mapFromRecord + */ +export function mapFromRecord(key: Schema, value: Schema): Schema> { + return Schema.record(key, value).transform( + map(key, value), + (input) => { + const out = new Map(); + for (const [k, v] of Object.entries(input)) { + out.set(k as K, v as V); + } + return out; + }, + (input) => { + const out = {} as Record; + input.forEach((v, k) => { + out[k] = v; + }); + return out; + }, + ); +} + +/** + * @tsplus derive fncts.schema.Schema[fncts.Map]<_> 10 + */ +export function deriveMap>( + // @ts-expect-error + ...[key, value]: [A] extends [Map] + ? Check> & Check.Extends> extends Check.True + ? [key: Schema, value: Schema] + : never + : never +): Schema { + return unsafeCoerce(mapFromRecord(key as Schema, value)); +} + +function mapParser(isDecoding: boolean) { + return (key: Schema, value: Schema): Parser> => { + const schema = map(key, value); + return Parser.make((u, options) => { + if (!(u instanceof Map)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); + } + const allErrors = options?.allErrors; + const errors = Vector.emptyPushable(); + const out = new Map(); + const keyParser = isDecoding ? key.decode : key.encode; + const valueParser = isDecoding ? value.decode : value.encode; + for (const [k, v] of u) { + const tk = keyParser(k, options); + Either.concrete(tk); + if (tk.isLeft()) { + errors.push(ParseError.KeyError(key.ast, k, tk.left)); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + } + const tv = valueParser(v, options); + Either.concrete(tv); + if (tv.isLeft()) { + errors.push(ParseError.KeyError(key.ast, k, ParseError.TypeError(value.ast, tv.left))); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + } + if (tk.isLeft() || tv.isLeft()) { + continue; + } + out.set(tk.right, tv.right); + } + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(out); + }); + }; +} + +function gen(key: Gen, value: Gen): Gen> { + return Gen.array(Gen.tuple(key, value)).map((pairs) => new Map(pairs)); +} diff --git a/packages/schema/src/Schema/api/maybe.ts b/packages/schema/src/Schema/api/maybe.ts index 2726b9fc..ba1363a1 100644 --- a/packages/schema/src/Schema/api/maybe.ts +++ b/packages/schema/src/Schema/api/maybe.ts @@ -5,11 +5,9 @@ import type { Check } from "@fncts/typelevel"; * @tsplus getter fncts.schema.Schema maybe */ export function maybe(value: Schema): Schema> { - return Schema.declaration( - Vector(value), - maybeInline(value), - maybeParser, - ASTAnnotationMap.empty.annotate(ASTAnnotation.Identifier, "Maybe"), + return Schema.declaration(Vector(value), maybeParser(true), maybeParser(false)).annotate( + ASTAnnotation.Identifier, + `Maybe<${value.show()}>`, ); } @@ -36,33 +34,20 @@ export function deriveMaybe>( return unsafeCoerce(maybeFromNullable(value)); } -function maybeParser(value: Schema): Parser> { - const schema = maybe(value); - return Parser.make((u, options) => { - if (!Maybe.isMaybe(u)) { - return ParseResult.fail(ParseError.TypeError(schema.ast, u)); - } - Maybe.concrete(u); - if (u.isNothing()) { - return ParseResult.succeed(Nothing()); - } else { - return value.decode(u.value, options).map((a) => Just(a)); - } - }); -} - -function maybeInline(value: Schema): Schema> { - return Schema.union( - Schema.struct({ - _tag: Schema.literal("Nothing"), - [Symbol.equals]: Schema.any, - [Symbol.hash]: Schema.any, - }), - Schema.struct({ - _tag: Schema.literal("Just"), - value, - [Symbol.equals]: Schema.any, - [Symbol.hash]: Schema.any, - }), - ) as unknown as Schema>; +function maybeParser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = maybe(value); + const parseValue = isDecoding ? value.decode : value.encode; + return Parser.make((u, options) => { + if (!Maybe.isMaybe(u)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); + } + Maybe.concrete(u); + if (u.isNothing()) { + return ParseResult.succeed(Nothing()); + } else { + return parseValue(u.value, options).map((a) => Just(a)); + } + }); + }; } diff --git a/packages/schema/src/Schema/api/set.ts b/packages/schema/src/Schema/api/set.ts new file mode 100644 index 00000000..d4dffee4 --- /dev/null +++ b/packages/schema/src/Schema/api/set.ts @@ -0,0 +1,74 @@ +import type { KeyError } from "@fncts/schema/ParseError"; +import type { Sized } from "@fncts/test/control/Sized"; + +/** + * @tsplus static fncts.schema.SchemaOps map + */ +export function set(value: Schema): Schema> { + return Schema.declaration(Vector(value), setParser(true), setParser(false)) + .annotate(ASTAnnotation.Identifier, `Set<${value.show()}>`) + .annotate(ASTAnnotation.GenHook, gen); +} + +/** + * @tsplus static fncts.schema.SchemaOps mapFromRecord + */ +export function setFromArray(value: Schema): Schema> { + return Schema.array(value).transform( + set(value), + (input) => { + return new Set(input); + }, + (input) => { + return Array.from(input); + }, + ); +} + +/** + * @tsplus derive fncts.schema.Schema[fncts.Set]<_> 10 + */ +export function deriveSet>( + ...[value]: [A] extends [Set] + ? Check>> extends Check.True + ? [value: Schema] + : never + : never +): Schema { + return unsafeCoerce(setFromArray(value)); +} + +function setParser(isDecoding: boolean) { + return (value: Schema): Parser> => { + const schema = set(value); + const parseValue = isDecoding ? value.decode : value.encode; + return Parser.make((u, options) => { + if (!(u instanceof Set)) { + return ParseResult.fail(ParseError.TypeError(schema.ast, u)); + } + + const allErrors = options?.allErrors; + const errors = Vector.emptyPushable(); + const out = new Set(); + for (const v of u) { + const tv = parseValue(v, options); + Either.concrete(tv); + if (tv.isLeft()) { + errors.push(ParseError.KeyError(value.ast, value, tv.left)); + if (!allErrors) { + return ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)); + } + continue; + } + out.add(tv.right); + } + return errors.isNonEmpty() + ? ParseResult.fail(ParseError.IterableError(schema.ast, u, errors)) + : ParseResult.succeed(out); + }); + }; +} + +function gen(value: Gen): Gen> { + return Gen.array(value).map((values) => new Set(values)); +} diff --git a/packages/schema/src/Show.ts b/packages/schema/src/Show.ts index 178b1e86..080d3562 100644 --- a/packages/schema/src/Show.ts +++ b/packages/schema/src/Show.ts @@ -1,169 +1,117 @@ -import type { TemplateLiteral, TemplateLiteralSpan } from "@fncts/schema/AST"; +import type { + Element, + Refinement, + StringKeyword, + SymbolKeyword, + TemplateLiteral, + TemplateLiteralSpan, + Tuple, + TypeLiteral, +} from "@fncts/schema/AST"; import { globalValue } from "@fncts/base/data/Global"; import { ASTTag } from "@fncts/schema/AST"; -import { ASTAnnotation } from "@fncts/schema/ASTAnnotation"; -import { memoize } from "@fncts/schema/utils"; +import { formatUnknown } from "@fncts/schema/utils"; -const showMemoMap = globalValue(Symbol.for("fncts.schema.Guard.showMemoMap"), () => new WeakMap>()); +const showMemoMap = globalValue(Symbol.for("fncts.schema.Guard.showMemoMap"), () => new WeakMap()); +const showMemoMapVerbose = globalValue( + Symbol.for("fncts.schema.Guard.showMemoMapVerbose"), + () => new WeakMap(), +); -function goMemo(ast: AST): Eval { - const memo = showMemoMap.get(ast); +function goMemo(ast: AST, verbose: boolean): string { + const memoMap = verbose ? showMemoMapVerbose : showMemoMap; + const memo = memoMap.get(ast); if (memo) { return memo; } - const s = go(ast); - showMemoMap.set(ast, s); + const s = go(ast, verbose); + memoMap.set(ast, s); return s; } /** - * @tsplus getter fncts.schema.Schema show + * @tsplus pipeable fncts.schema.Schema show */ -export function show(self: Schema): string { - const ev = goMemo(self.ast); - return ev.run; +export function show(verbose: boolean = false) { + return (self: Schema): string => goMemo(self.ast, verbose); } -function go(ast: AST): Eval { +/** + * @tsplus pipeable fncts.schema.AST show + */ +export function showAST(verbose: boolean = false) { + return (self: AST): string => goMemo(self, verbose); +} + +function go(ast: AST, verbose: boolean): string { AST.concrete(ast); switch (ast._tag) { case ASTTag.Declaration: { - return ast.annotations.get(ASTAnnotation.Identifier).match( - () => Eval.now("Unknown Type"), - (id) => { - return ast.typeParameters - .traverse(Eval.Applicative)(goMemo) - .map((ts) => { - if (ts.length <= 0) { - return id; - } else { - return `${id}<${ts.join(", ")}>`; - } - }); - }, - ); - } - case ASTTag.Literal: { - if (ast.literal === null) { - return Eval.now("null"); - } else { - return Eval.now(ast.literal.toString()); - } + return ast.getFormattedExpected(verbose).getOrElse(""); } + case ASTTag.Literal: + return ast.getFormattedExpected(verbose).getOrElse(formatUnknown(ast.literal)); case ASTTag.UniqueSymbol: - return Eval.now(ast.symbol.toString()); + return ast.getFormattedExpected(verbose).getOrElse(formatUnknown(ast.symbol)); case ASTTag.UndefinedKeyword: - return Eval.now("undefined"); + return ast.getFormattedExpected(verbose).getOrElse("undefined"); case ASTTag.VoidKeyword: - return Eval.now("void"); + return ast.getFormattedExpected(verbose).getOrElse("void"); case ASTTag.NeverKeyword: - return Eval.now("never"); + return ast.getFormattedExpected(verbose).getOrElse("never"); case ASTTag.UnknownKeyword: - return Eval.now("unknown"); + return ast.getFormattedExpected(verbose).getOrElse("unknown"); case ASTTag.AnyKeyword: - return Eval.now("any"); + return ast.getFormattedExpected(verbose).getOrElse("any"); case ASTTag.StringKeyword: - return Eval.now("string"); + return ast.getFormattedExpected(verbose).getOrElse("string"); case ASTTag.NumberKeyword: - return Eval.now("number"); + return ast.getFormattedExpected(verbose).getOrElse("number"); case ASTTag.BooleanKeyword: - return Eval.now("boolean"); + return ast.getFormattedExpected(verbose).getOrElse("boolean"); case ASTTag.BigIntKeyword: - return Eval.now("bigint"); + return ast.getFormattedExpected(verbose).getOrElse("bigint"); case ASTTag.SymbolKeyword: - return Eval.now("symbol"); + return ast.getFormattedExpected(verbose).getOrElse("symbol"); case ASTTag.ObjectKeyword: - return Eval.now("object"); + return ast.getFormattedExpected(verbose).getOrElse("object"); case ASTTag.TemplateLiteral: - return Eval.now("`" + formatTemplateLiteral(ast) + "`"); - case ASTTag.Tuple: - return Do((Δ) => { - const elements = Δ(ast.elements.traverse(Eval.Applicative)((element) => goMemo(element.type))); - const restElements = Δ( - ast.rest.match( - () => Eval.now(Vector.empty()), - (rest) => rest.traverse(Eval.Applicative)(goMemo), - ), - ); - - return Δ( - Eval(() => { - if (elements.length === 0 && restElements.length === 1) { - if (ast.isReadonly) { - return `ReadonlyArray<${restElements[0]}>`; - } else { - return `Array<${restElements[0]}>`; - } - } - - const prefix = (ast.isReadonly ? "readonly " : "") + "[" + elements.join(", "); - const middle = restElements.length > 0 ? ", " : ""; - const suffix = restElements.map((s) => `...${s}`).join(", ") + "]"; - return prefix + middle + suffix; - }), - ); - }); - case ASTTag.TypeLiteral: - return Do((Δ) => { - const propertySignatures = Δ(ast.propertySignatures.traverse(Eval.Applicative)((ps) => goMemo(ps.type))); - const indexSignatures = Δ( - ast.indexSignatures.traverse(Eval.Applicative)((is) => goMemo(is.parameter).zip(goMemo(is.type))), - ); - - const required: Array<[PropertyKey, string]> = []; - const optional: Array<[PropertyKey, string]> = []; - - ast.propertySignatures.forEachWithIndex((i, ps) => { - const name = ps.name; - if (!ps.isOptional) { - required.push([name, propertySignatures[i]!]); - } else { - optional.push([name, propertySignatures[i]!]); - } - }); - - const prefix = "{"; - const properties = required - .concat(optional) - .sort(([k1], [k2]) => k1.toLocaleString().localeCompare(k2.toLocaleString())) - .map(([propertyKey, type]) => `${String(propertyKey)}: ${type}`) - .join(", "); - const index = indexSignatures.map(([param, type]) => `[x: ${param}]: ${type}`).join(", "); - const suffix = "}"; - - return prefix + " " + properties + (index.length === 0 ? "" : ", ") + index + " " + suffix; - }); - case ASTTag.Union: - return ast.types - .traverse(Eval.Applicative)(goMemo) - .map((ts) => ts.join(" | ")); + return ast.getFormattedExpected(verbose).getOrElse(formatTemplateLiteral(ast)); + case ASTTag.Tuple: { + return ast.getFormattedExpected(verbose).getOrElse(formatTuple(ast, verbose)); + } + case ASTTag.TypeLiteral: { + return ast.getFormattedExpected(verbose).getOrElse(formatTypeLiteral(ast, verbose)); + } + case ASTTag.Union: { + return ast.getFormattedExpected(verbose).getOrElse(ast.types.map((ast) => goMemo(ast, verbose)).join(" | ")); + } case ASTTag.Lazy: { - const f = () => goMemo(ast.getAST()); - const get = memoize>(f); - return Eval.defer(() => get()); + return ast + .getFormattedExpected(verbose) + .orElse(Maybe.tryCatch(ast.getAST).flatMap((ast) => ast.getFormattedExpected(verbose))) + .getOrElse(""); } case ASTTag.Enum: { - return Eval.now(ast.enums.map(([name]) => name).join(" | ")); + return ast + .getFormattedExpected(verbose) + .getOrElse( + ` JSON.stringify(value)).join(" | ")}`, + ); } case ASTTag.Refinement: { - return ast.annotations.get(ASTAnnotation.Identifier).match( - () => goMemo(ast.from).map((from) => `Refined<${from}>`), - (id) => Eval.now(id), - ); + return ast.getFormattedExpected(verbose).getOrElse(`{ ${goMemo(ast.from, verbose)} | filter }`); + } + case ASTTag.Transform: { + return ast + .getFormattedExpected(verbose) + .getOrElse(`(${goMemo(ast.from, verbose)} <-> ${goMemo(ast.to, verbose)})`); } - case ASTTag.Transform: - return goMemo(ast.to); case ASTTag.Validation: { - return goMemo(ast.from).map((from) => { - const validationNames = ast.validation.map((v) => v.name).join(" & "); - - if (validationNames.length <= 0) { - return from; - } - - return `${from} & ${validationNames}`; - }); + return ast + .getFormattedExpected(verbose) + .getOrElse(`${goMemo(ast.from, verbose)} (${ast.validation.map((v) => v.name).join(" & ")})`); } } } @@ -178,5 +126,85 @@ function formatTemplateLiteralSpan(span: TemplateLiteralSpan): string { } function formatTemplateLiteral(ast: TemplateLiteral): string { - return ast.head + ast.spans.map((span) => formatTemplateLiteralSpan(span) + span.literal).join(""); + return "`" + ast.head + ast.spans.map((span) => formatTemplateLiteralSpan(span) + span.literal).join("") + "`"; +} + +function formatElement(ast: Element, verbose: boolean): string { + return goMemo(ast.type, verbose) + (ast.isOptional ? "?" : ""); +} + +function getParameterBase( + self: StringKeyword | SymbolKeyword | TemplateLiteral | Refinement, +): StringKeyword | SymbolKeyword | TemplateLiteral { + switch (self._tag) { + case ASTTag.StringKeyword: + case ASTTag.SymbolKeyword: + case ASTTag.TemplateLiteral: + return self; + case ASTTag.Refinement: + return getParameterBase(self); + } +} + +function formatTuple(ast: Tuple, verbose: boolean): string { + const formattedElements = ast.elements.map((element) => formatElement(element, verbose)).join(", "); + return ast.rest + .filter((rest) => rest.isNonEmpty()) + .match( + () => `${ast.isReadonly ? "readonly " : ""}[${formattedElements}]`, + (rest) => { + const head = rest.unsafeHead!; + const tail = rest.tail; + const formattedHead = goMemo(head, verbose); + const wrappedHead = formattedHead.includes(" | ") ? `(${formattedHead})` : formattedHead; + + if (tail.length > 0) { + const formattedTail = tail.map((ast) => goMemo(ast, verbose)).join(", "); + if (ast.elements.length > 0) { + return `${ast.isReadonly ? "readonly " : " "}[${formattedElements}, ...${wrappedHead}[], ${formattedTail}]`; + } else { + return `${ast.isReadonly ? "readonly " : " "}[...${wrappedHead}[], ${formattedTail}]`; + } + } else { + if (ast.elements.length > 0) { + return `${ast.isReadonly ? "readonly " : " "}[${formattedElements}, ...${wrappedHead}[]]`; + } else { + return `${ast.isReadonly ? "Readonly" : ""}Array<${formattedHead}>`; + } + } + }, + ); +} + +function formatTypeLiteral(ast: TypeLiteral, verbose: boolean): string { + const formattedPropertySignatures = ast.propertySignatures + .map( + (ps) => + (ps.isReadonly ? "readonly " : "") + + String(ps.name) + + (ps.isOptional ? "?" : "") + + ": " + + goMemo(ps.type, verbose), + ) + .join("; "); + if (ast.indexSignatures.length > 0) { + const formattedIndexSignatures = ast.indexSignatures + .map( + (is) => + (is.isReadonly ? "readonly " : "") + + `[x: ${goMemo(getParameterBase(is.parameter), verbose)}]: ${goMemo(is.type, verbose)}`, + ) + .join("; "); + if (ast.propertySignatures.length > 0) { + return `{ ${formattedPropertySignatures}; ${formattedIndexSignatures} }`; + } else { + return `{ ${formattedIndexSignatures} }`; + } + } else { + if (ast.propertySignatures.length > 0) { + return `{ ${formattedPropertySignatures} }`; + } else { + return "{}"; + } + } } diff --git a/packages/schema/src/global.ts b/packages/schema/src/global.ts index 99aa573d..4a401180 100644 --- a/packages/schema/src/global.ts +++ b/packages/schema/src/global.ts @@ -39,10 +39,6 @@ import { ASTAnnotationMap } from "@fncts/schema/ASTAnnotationMap"; * @tsplus global */ import { ParseError } from "@fncts/schema/ParseError"; -/** - * @tsplus global - */ -import { ParseFailure } from "@fncts/schema/ParseFailure"; /** * @tsplus global */ diff --git a/packages/schema/src/utils.ts b/packages/schema/src/utils.ts index c348db0a..8366ca1f 100644 --- a/packages/schema/src/utils.ts +++ b/packages/schema/src/utils.ts @@ -1,6 +1,7 @@ import type { IndexSignature, TemplateLiteral } from "./AST"; import { ASTTag } from "./AST.js"; +import { showWithOptions } from "@fncts/base/data/Showable"; export function memoize(f: (a: A) => B): (a: A) => B { const cache = new Map(); @@ -46,3 +47,7 @@ export function getKeysForIndexSignature( return getKeysForIndexSignature(input, parameter.from as any); } } + +export function formatUnknown(u: unknown): string { + return showWithOptions(u, {}); +} diff --git a/packages/schema/test/Schema/array.test.ts b/packages/schema/test/Schema/array.test.ts index 90d12b2f..b3e54726 100644 --- a/packages/schema/test/Schema/array.test.ts +++ b/packages/schema/test/Schema/array.test.ts @@ -1,4 +1,6 @@ -import { IndexError, TypeError } from "@fncts/schema/ParseError"; +import type { Tuple } from "@fncts/schema/AST"; + +import { IndexError, TupleError, TypeError } from "@fncts/schema/ParseError"; import { expectFailure, expectSuccess } from "../utils.js"; @@ -10,15 +12,19 @@ suite("Array Schema", () => { test("failure", () => { const schema = Schema.string.array; - expectFailure(schema, 0, Vector(TypeError(AST.unknownArray, 0))); + expectFailure(schema, 0, TypeError(AST.unknownArray, 0)); expectFailure( schema, [1, 2, 3], - Vector( - IndexError(0, Vector(TypeError(AST.stringKeyword, 1))), - IndexError(1, Vector(TypeError(AST.stringKeyword, 2))), - IndexError(2, Vector(TypeError(AST.stringKeyword, 3))), + TupleError( + schema.ast as Tuple, + [1, 2, 3], + Vector( + IndexError(0, TypeError(AST.stringKeyword, 1)), + IndexError(1, TypeError(AST.stringKeyword, 2)), + IndexError(2, TypeError(AST.stringKeyword, 3)), + ), ), { allErrors: true }, ); diff --git a/packages/schema/test/Schema/nullable.test.ts b/packages/schema/test/Schema/nullable.test.ts index 69b80de8..b310e0c2 100644 --- a/packages/schema/test/Schema/nullable.test.ts +++ b/packages/schema/test/Schema/nullable.test.ts @@ -1,3 +1,5 @@ +import type { Union } from "@fncts/schema/AST"; + import { expectFailure, expectSuccess } from "../utils.js"; suite("Nullable Schema", () => { @@ -14,18 +16,29 @@ suite("Nullable Schema", () => { expectFailure( schema, 42, - Vector( - ParseError.UnionMemberError(Vector(ParseError.TypeError(AST.createLiteral(null), 42))), - ParseError.UnionMemberError(Vector(ParseError.TypeError(AST.stringKeyword, 42))), + ParseError.UnionError( + schema.ast as Union, + 42, + Vector( + ParseError.UnionMemberError(AST.createLiteral(null), ParseError.TypeError(AST.createLiteral(null), 42)), + ParseError.UnionMemberError(AST.stringKeyword, ParseError.TypeError(AST.stringKeyword, 42)), + ), ), ); expectFailure( schema, undefined, - Vector( - ParseError.UnionMemberError(Vector(ParseError.TypeError(AST.createLiteral(null), undefined))), - ParseError.UnionMemberError(Vector(ParseError.TypeError(AST.stringKeyword, undefined))), + ParseError.UnionError( + schema.ast as Union, + undefined, + Vector( + ParseError.UnionMemberError( + AST.createLiteral(null), + ParseError.TypeError(AST.createLiteral(null), undefined), + ), + ParseError.UnionMemberError(AST.stringKeyword, ParseError.TypeError(AST.stringKeyword, undefined)), + ), ), ); }); diff --git a/packages/schema/test/Schema/primitives.test.ts b/packages/schema/test/Schema/primitives.test.ts index 46e8f780..2881458b 100644 --- a/packages/schema/test/Schema/primitives.test.ts +++ b/packages/schema/test/Schema/primitives.test.ts @@ -7,34 +7,34 @@ import { expectFailure, expectSuccess } from "../utils.js"; suite("Schema Primitives", () => { test("never", () => { const schema = Schema.never; - expectFailure(schema, 1, Vector(TypeError(schema.ast, 1))); + expectFailure(schema, 1, TypeError(schema.ast, 1)); }); test("number", () => { const schema = Schema.number; expectSuccess(schema, 1, 1); - expectFailure(schema, "a", Vector(TypeError(schema.ast, "a"))); + expectFailure(schema, "a", TypeError(schema.ast, "a")); }); test("string", () => { const schema = Schema.string; expectSuccess(schema, "hello", "hello"); - expectFailure(schema, 1, Vector(TypeError(schema.ast, 1))); + expectFailure(schema, 1, TypeError(schema.ast, 1)); }); test("boolean", () => { const schema = Schema.boolean; expectSuccess(schema, true, true); expectSuccess(schema, false, false); - expectFailure(schema, 1, Vector(TypeError(schema.ast, 1))); - expectFailure(schema, 0, Vector(TypeError(schema.ast, 0))); + expectFailure(schema, 1, TypeError(schema.ast, 1)); + expectFailure(schema, 0, TypeError(schema.ast, 0)); }); test("bigint", () => { const schema = Schema.bigint; expectSuccess(schema, 1n, 1n); expectSuccess(schema, BigInt(1), BigInt(1)); - expectFailure(schema, 1, Vector(TypeError(schema.ast, 1))); + expectFailure(schema, 1, TypeError(schema.ast, 1)); }); test("any", () => { @@ -50,33 +50,33 @@ suite("Schema Primitives", () => { test("undefined", () => { const schema = Schema.undefined; expectSuccess(schema, undefined, undefined); - expectFailure(schema, null, Vector(TypeError(schema.ast, null))); - expectFailure(schema, 42, Vector(TypeError(schema.ast, 42))); - expectFailure(schema, {}, Vector(TypeError(schema.ast, {}))); + expectFailure(schema, null, TypeError(schema.ast, null)); + expectFailure(schema, 42, TypeError(schema.ast, 42)); + expectFailure(schema, {}, TypeError(schema.ast, {})); }); test("null", () => { const schema = Schema.null; expectSuccess(schema, null, null); - expectFailure(schema, undefined, Vector(TypeError(schema.ast, undefined))); - expectFailure(schema, 42, Vector(TypeError(schema.ast, 42))); - expectFailure(schema, {}, Vector(TypeError(schema.ast, {}))); + expectFailure(schema, undefined, TypeError(schema.ast, undefined)); + expectFailure(schema, 42, TypeError(schema.ast, 42)); + expectFailure(schema, {}, TypeError(schema.ast, {})); }); test("symbol", () => { const schema = Schema.symbol; const symbol = Symbol(); expectSuccess(schema, symbol, symbol); - expectFailure(schema, "symbol", Vector(TypeError(schema.ast, "symbol"))); + expectFailure(schema, "symbol", TypeError(schema.ast, "symbol")); }); test("object", () => { const schema = Schema.object; expectSuccess(schema, {}, {}); expectSuccess(schema, { a: 1 }, { a: 1 }); - expectFailure(schema, null, Vector(TypeError(schema.ast, null))); - expectFailure(schema, undefined, Vector(TypeError(schema.ast, undefined))); - expectFailure(schema, 42, Vector(TypeError(schema.ast, 42))); + expectFailure(schema, null, TypeError(schema.ast, null)); + expectFailure(schema, undefined, TypeError(schema.ast, undefined)); + expectFailure(schema, 42, TypeError(schema.ast, 42)); }); test("date", () => { @@ -86,19 +86,13 @@ suite("Schema Primitives", () => { expectFailure( schema, date.toISOString(), - Vector( - RefinementError( - schema.ast as Refinement, - date.toISOString(), - "From", - Vector(TypeError(schema.ast.getFrom, date.toISOString())), - ), + RefinementError( + schema.ast as Refinement, + date.toISOString(), + "From", + TypeError(schema.ast.getFrom, date.toISOString()), ), ); - expectFailure( - schema, - {}, - Vector(RefinementError(schema.ast as Refinement, {}, "Predicate", Vector(TypeError(schema.ast, {})))), - ); + expectFailure(schema, {}, RefinementError(schema.ast as Refinement, {}, "Predicate", TypeError(schema.ast, {}))); }); }); diff --git a/packages/schema/test/Schema/record.test.ts b/packages/schema/test/Schema/record.test.ts new file mode 100644 index 00000000..46f1d392 --- /dev/null +++ b/packages/schema/test/Schema/record.test.ts @@ -0,0 +1,29 @@ +import type { TypeLiteral } from "@fncts/schema/AST"; + +import { expectFailure, expectSuccess } from "../utils.js"; + +suite("Record Schema", () => { + test("success", () => { + const schema = Schema.record(Schema.string, Schema.string); + + expectSuccess(schema, { a: "a", b: "b" }, { a: "a", b: "b" }); + }); + + test("failure", () => { + const schema = Schema.record(Schema.string, Schema.string); + + expectFailure( + schema, + { a: 1, b: 2 }, + ParseError.TypeLiteralError( + schema.ast as TypeLiteral, + { a: 1, b: 2 }, + Vector( + ParseError.KeyError(AST.createLiteral("a"), "a", ParseError.TypeError(AST.stringKeyword, 1)), + ParseError.KeyError(AST.createLiteral("b"), "b", ParseError.TypeError(AST.stringKeyword, 2)), + ), + ), + { allErrors: true }, + ); + }); +}); diff --git a/packages/schema/test/Schema/struct.test.ts b/packages/schema/test/Schema/struct.test.ts index 05949071..4185ef36 100644 --- a/packages/schema/test/Schema/struct.test.ts +++ b/packages/schema/test/Schema/struct.test.ts @@ -1,3 +1,5 @@ +import type { TypeLiteral } from "@fncts/schema/AST"; + import { expectFailure, expectSuccess } from "../utils.js"; suite("Struct Schema", () => { @@ -21,15 +23,25 @@ suite("Struct Schema", () => { expectFailure( schema, { a: 42, b: 42 }, - Vector(ParseError.KeyError(AST.createLiteral("b"), "b", Vector(ParseError.TypeError(AST.stringKeyword, 42)))), + ParseError.TypeLiteralError( + schema.ast as TypeLiteral, + { a: 42, b: 42 }, + Vector(ParseError.KeyError(AST.createLiteral("b"), "b", ParseError.TypeError(AST.stringKeyword, 42))), + { a: 42 }, + ), ); - expectFailure(schema, 42, Vector(ParseError.TypeError(AST.unknownRecord, 42))); + expectFailure(schema, 42, ParseError.TypeError(AST.unknownRecord, 42)); expectFailure( schema, { a: 42, b: "hello", c: "unexpected" }, - Vector(ParseError.KeyError(AST.createLiteral("c"), "c", Vector(ParseError.UnexpectedError("unexpected")))), + ParseError.TypeLiteralError( + schema.ast as TypeLiteral, + { a: 42, b: "hello", c: "unexpected" }, + Vector(ParseError.KeyError(AST.createLiteral("c"), "c", ParseError.UnexpectedError("unexpected"))), + { a: 42, b: "hello" }, + ), ); }); }); diff --git a/packages/schema/test/Schema/union.test.ts b/packages/schema/test/Schema/union.test.ts index 0d4b2adc..2455d512 100644 --- a/packages/schema/test/Schema/union.test.ts +++ b/packages/schema/test/Schema/union.test.ts @@ -1,3 +1,5 @@ +import type { TypeLiteral, Union } from "@fncts/schema/AST"; + import { expectFailure, expectSuccess } from "../utils.js"; suite("Union Schema", () => { @@ -9,20 +11,36 @@ suite("Union Schema", () => { }); test("failure", () => { - const schema = Schema.union(Schema.struct({ a: Schema.number }), Schema.struct({ b: Schema.string })); + const schemaA = Schema.struct({ a: Schema.number }); + const schemaB = Schema.struct({ b: Schema.string }); + const schema = Schema.union(schemaA, schemaB); expectFailure( schema, { b: 1 }, - Vector( - ParseError.UnionMemberError( - Vector( - ParseError.KeyError(AST.createLiteral("a"), "a", Vector(ParseError.MissingError)), - ParseError.KeyError(AST.createLiteral("b"), "b", Vector(ParseError.UnexpectedError(1))), + ParseError.UnionError( + schema.ast as Union, + { b: 1 }, + Vector( + ParseError.UnionMemberError( + schemaA.ast, + ParseError.TypeLiteralError( + schemaA.ast as TypeLiteral, + { b: 1 }, + Vector( + ParseError.KeyError(AST.createLiteral("a"), "a", ParseError.MissingError), + ParseError.KeyError(AST.createLiteral("b"), "b", ParseError.UnexpectedError(1)), + ), + ), + ), + ParseError.UnionMemberError( + schemaB.ast, + ParseError.TypeLiteralError( + schemaB.ast as TypeLiteral, + { b: 1 }, + Vector(ParseError.KeyError(AST.createLiteral("b"), "b", ParseError.TypeError(AST.stringKeyword, 1))), + ), ), - ), - ParseError.UnionMemberError( - Vector(ParseError.KeyError(AST.createLiteral("b"), "b", Vector(ParseError.TypeError(AST.stringKeyword, 1)))), ), ), { allErrors: true }, diff --git a/packages/schema/test/utils.ts b/packages/schema/test/utils.ts index 88b88d66..ed1d11d6 100644 --- a/packages/schema/test/utils.ts +++ b/packages/schema/test/utils.ts @@ -13,15 +13,10 @@ export function expectSuccess(self: Schema, input: unknown, output: A, opt ); } -export function expectFailure( - self: Schema, - input: unknown, - expected: Vector, - options?: ParseOptions, -) { +export function expectFailure(self: Schema, input: unknown, expected: ParseError, options?: ParseOptions) { self.decode(input, options).match( (e) => { - assert.deepEqual(e.errors, expected); + assert.deepEqual(e, expected); }, (a) => { assert.fail(`Expected failure, got ${a}`);