import { describe, it } from "@effect/vitest" import { BigDecimal, Brand, Cause, Chunk, Context, DateTime, Deferred, Duration, Effect, Equal, Exit, Fiber, flow, HashMap, HashSet, Option, Order, pipe, Predicate, Redacted, Result, Schema, SchemaAST, SchemaGetter, SchemaIssue, SchemaParser, SchemaTransformation, String as Str, Struct, Tuple } from "effect" import { TestSchema } from "effect/testing" import { produce } from "immer" import { deepStrictEqual, fail, ok, strictEqual } from "node:assert" import { assertFalse, assertInclude, assertTrue, throws } from "../utils/assert.ts" const verifyGeneration = true const equals = TestSchema.Asserts.ast.fields.equals const SnakeToCamel = Schema.String.pipe( Schema.decode( SchemaTransformation.snakeToCamel() ) ) describe("Schema", () => { it("isSchema", () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) {} class B extends Schema.Opaque()(Schema.Struct({ a: Schema.String })) {} assertTrue(Schema.isSchema(Schema.String)) assertTrue(Schema.isSchema(A)) assertTrue(Schema.isSchema(B)) assertFalse(Schema.isSchema({})) }) it("toString", () => { const schema = Schema.String const result = Schema.decodeUnknownExit(schema)(null) assertTrue(Exit.isFailure(result)) strictEqual(String(result.cause.reasons[0]), "Fail(SchemaError(Expected string, got null))") }) describe("SchemaError", () => { it("extends Error and exposes the issue", () => { const result = SchemaParser.decodeUnknownResult(Schema.String)(null) assertTrue(Result.isFailure(result)) const error = new Schema.SchemaError(result.failure) assertTrue(error instanceof Error) assertTrue(Schema.isSchemaError(error)) strictEqual(error._tag, "SchemaError") strictEqual(error.name, "SchemaError") strictEqual(error.issue, result.failure) strictEqual(error.message, "Expected string, got null") strictEqual(String(error), "SchemaError(Expected string, got null)") }) }) describe("parseOptions annotation", () => { it("Number", async () => { const schema = Schema.Number.check(Schema.isGreaterThan(0), Schema.isInt()).annotate({ parseOptions: { errors: "all" } }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( -1.2, `Expected a value greater than 0, got -1.2 Expected an integer, got -1.2` ) }) it("Struct", async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Struct({ c: Schema.String, d: Schema.String }).annotate({ parseOptions: { errors: "first" } }) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( { a: "a", b: {} }, `Missing key at ["b"]["c"]` ) }) it("should not read parseOptions from encodingChecks", async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.String }).pipe( Schema.flip, Schema.check(Schema.isMaxProperties(1)), Schema.annotate({ parseOptions: { errors: "first" } }), Schema.flip ) assertTrue(SchemaAST.isObjects(schema.ast)) strictEqual(schema.ast.checks, undefined) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( {}, `Missing key at ["a"] Missing key at ["b"]` ) }) }) describe("parse options", () => { it("decoders can receive options when they are created", () => { const schema = Schema.Struct({ a: Schema.String }) const decode = Schema.decodeUnknownExit(schema, { onExcessProperty: "error" }) const failure = decode({ a: "a", b: "b" }) assertTrue(Exit.isFailure(failure)) const success = decode({ a: "a", b: "b" }, { onExcessProperty: "preserve" }) assertTrue(Exit.isSuccess(success)) deepStrictEqual(success.value, { a: "a", b: "b" }) }) it("encoders can receive options when they are created", () => { const schema = Schema.Struct({ a: Schema.String }) const encode = Schema.encodeUnknownExit(schema, { onExcessProperty: "error" }) const failure = encode({ a: "a", b: "b" }) assertTrue(Exit.isFailure(failure)) const success = encode({ a: "a", b: "b" }, { onExcessProperty: "preserve" }) assertTrue(Exit.isSuccess(success)) deepStrictEqual(success.value, { a: "a", b: "b" }) }) }) describe("Literal", () => { it("should throw an error if the literal is not a finite number", () => { throws( () => Schema.Literal(Infinity), new Error("A numeric literal must be finite, got Infinity") ) throws( () => Schema.Literal(-Infinity), new Error("A numeric literal must be finite, got -Infinity") ) throws( () => Schema.Literal(NaN), new Error("A numeric literal must be finite, got NaN") ) }) it("should expose the literal", () => { const schema = Schema.Literal("a") strictEqual(schema.literal, "a") strictEqual(schema.annotate({}).literal, "a") }) it(`"a"`, async () => { const schema = Schema.Literal("a") const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed("a") await make.fail(null, `Expected "a", got null`) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(1, `Expected "a", got 1`) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail(1, `Expected "a", got 1`) }) it(`1`, async () => { const schema = Schema.Literal(1) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(1) await make.fail(null, `Expected 1, got null`) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail("1", `Expected 1, got "1"`) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail("1", `Expected 1, got "1"`) }) it("transform", async () => { const schema = Schema.Literal(0).transform("a") const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(0, "a") await decoding.fail(1, `Expected 0, got 1`) const encoding = asserts.encoding() await encoding.succeed("a", 0) await encoding.fail("b", `Expected "a", got "b"`) }) }) describe("Literals", () => { it("red, green, blue", async () => { const schema = Schema.Literals(["red", "green", "blue"]) deepStrictEqual(schema.literals, ["red", "green", "blue"]) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed("red") await make.succeed("green") await make.succeed("blue") await make.fail("yellow", `Expected "red" | "green" | "blue", got "yellow"`) }) it("transform", async () => { const schema = Schema.Literals([0, 1]).transform(["a", "b"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(0, "a") await decoding.succeed(1, "b") await decoding.fail(2, `Expected 0 | 1, got 2`) const encoding = asserts.encoding() await encoding.succeed("a", 0) await encoding.succeed("b", 1) await encoding.fail("c", `Expected "a" | "b", got "c"`) }) it("pick", () => { const schema = Schema.Literals(["a", "b", "c"]).pick(["a", "b"]) deepStrictEqual(schema.literals, ["a", "b"]) }) }) it("Never", async () => { const schema = Schema.Never const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.fail(null as never, `Expected never, got null`) const decoding = asserts.decoding() await decoding.fail("a", `Expected never, got "a"`) const encoding = asserts.encoding() await encoding.fail("a", `Expected never, got "a"`) }) it("Any", async () => { const schema = Schema.Any const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed("a") const decoding = asserts.decoding() await decoding.succeed("a") }) it("Unknown", async () => { const schema = Schema.Unknown const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed("a") const decoding = asserts.decoding() await decoding.succeed("a") }) it("Null", async () => { const schema = Schema.Null const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(null) await make.fail(undefined, `Expected null, got undefined`) }) it("Undefined", async () => { const schema = Schema.Undefined const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(undefined) await make.fail(null, `Expected undefined, got null`) }) it("String", async () => { const schema = Schema.String const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed("a") await make.fail(null, `Expected string, got null`) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(1, `Expected string, got 1`) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail(1, `Expected string, got 1`) }) it("Number", async () => { const schema = Schema.Number const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(1) await make.fail(null, `Expected number, got null`) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail("a", `Expected number, got "a"`) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail("a", `Expected number, got "a"`) }) it("Boolean", async () => { const schema = Schema.Boolean const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(true) await make.succeed(false) await make.fail(null, `Expected boolean, got null`) const decoding = asserts.decoding() await decoding.succeed(true) await decoding.succeed(false) await decoding.fail("a", `Expected boolean, got "a"`) const encoding = asserts.encoding() await encoding.succeed(true) await encoding.succeed(false) await encoding.fail("a", `Expected boolean, got "a"`) }) it("Symbol", async () => { const schema = Schema.Symbol const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(Symbol("a")) await make.fail(null, `Expected symbol, got null`) const decoding = asserts.decoding() await decoding.succeed(Symbol("a")) await decoding.fail("a", `Expected symbol, got "a"`) const encoding = asserts.encoding() await encoding.succeed(Symbol("a")) await encoding.fail("a", `Expected symbol, got "a"`) }) it("UniqueSymbol", async () => { const a = Symbol("a") const schema = Schema.UniqueSymbol(a) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(a) await make.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`) const decoding = asserts.decoding() await decoding.succeed(a) await decoding.fail(Symbol("b"), `Expected Symbol(a), got Symbol(b)`) }) it("BigInt", async () => { const schema = Schema.BigInt const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(1n) await make.fail(null, `Expected bigint, got null`) const decoding = asserts.decoding() await decoding.succeed(1n) await decoding.fail("1", `Expected bigint, got "1"`) const encoding = asserts.encoding() await encoding.succeed(1n) await encoding.fail("1", `Expected bigint, got "1"`) }) it("Void", async () => { const schema = Schema.Void const asserts = new TestSchema.Asserts(schema) // The public make input stays typed as void; these callbacks exercise runtime parser behavior. let fn: () => void const make = asserts.make() await make.succeed() await make.succeed(undefined) fn = () => undefined await make.succeed(fn()) fn = () => null await make.succeed(fn(), undefined) fn = () => "a" await make.succeed(fn(), undefined) const decoding = asserts.decoding() await decoding.succeed(undefined) await decoding.succeed(null, undefined) await decoding.succeed("a", undefined) const encoding = asserts.encoding() await encoding.succeed(undefined) await encoding.succeed(null, undefined) await encoding.succeed("1", undefined) }) it("ObjectKeyword", async () => { const schema = Schema.ObjectKeyword const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({}) await make.succeed([]) await make.fail(null, `Expected object | array | function, got null`) const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed([]) await decoding.fail("1", `Expected object | array | function, got "1"`) const encoding = asserts.encoding() await encoding.succeed({}) await encoding.succeed([]) await encoding.fail("1", `Expected object | array | function, got "1"`) }) it("optionalKey", () => { const schema = Schema.optionalKey(Schema.String) strictEqual(schema.ast.context?.isOptional, true) }) it("optionalKey & mutableKey", () => { const schema = Schema.String.pipe(Schema.optionalKey, Schema.mutableKey) strictEqual(schema.ast.context?.isOptional, true) strictEqual(schema.ast.context?.isMutable, true) }) it("optional", () => { const schema = Schema.optionalKey(Schema.String) strictEqual(schema.ast.context?.isOptional, true) }) it("mutableKey", () => { const schema = Schema.mutableKey(Schema.String) strictEqual(schema.ast.context?.isMutable, true) }) it("mutableKey & optionalKey", () => { const schema = Schema.String.pipe(Schema.mutableKey, Schema.optionalKey) strictEqual(schema.ast.context?.isOptional, true) strictEqual(schema.ast.context?.isMutable, true) }) it("readonlyKey", () => { const schema = Schema.readonlyKey(Schema.mutableKey(Schema.String)) strictEqual(schema.ast.context?.isMutable, undefined) }) describe("Struct", () => { it("should throw an error if there are duplicate property signatures", () => { throws( () => new SchemaAST.Objects( [ new SchemaAST.PropertySignature("a", Schema.String.ast), new SchemaAST.PropertySignature("b", Schema.String.ast), new SchemaAST.PropertySignature("c", Schema.String.ast), new SchemaAST.PropertySignature("a", Schema.String.ast), new SchemaAST.PropertySignature("c", Schema.String.ast) ], [] ), new Error(`Duplicate identifiers: ["a","c"]. ts(2300)`) ) }) describe("propertyOrder", () => { it("all required fields", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.String }) const input = { c: "c", b: "b", a: "a", d: "d" } const output = Schema.decodeUnknownSync(schema)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) deepStrictEqual(Object.keys(output), ["c", "b", "a", "d"]) }) it("optional field with default", () => { const schema = Schema.Struct({ a: Schema.String.pipe(Schema.encode({ decode: SchemaGetter.withDefault(Effect.succeed("default-a")), encode: SchemaGetter.passthrough() })), b: Schema.String }) const input = { c: "c", b: "b", d: "d" } const output = Schema.decodeUnknownSync(schema)(input, { propertyOrder: "original", onExcessProperty: "preserve" }) deepStrictEqual(Object.keys(output), ["c", "b", "d", "a"]) }) }) describe("onExcessProperty", () => { it("error", async () => { const schema = Schema.Struct({ a: Schema.String }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { onExcessProperty: "error" } }) await decoding.fail( { a: "a", b: "b" }, `Unexpected key with value "b" at ["b"]` ) const sym = Symbol("sym") await decoding.fail( { a: "a", [sym]: "sym" }, `Unexpected key with value "sym" at [Symbol(sym)]` ) const decodingAll = asserts.decoding({ parseOptions: { onExcessProperty: "error", errors: "all" } }) await decodingAll.fail( { a: "a", b: "b", c: "c" }, `Unexpected key with value "b" at ["b"] Unexpected key with value "c" at ["c"]` ) }) it("preserve", async () => { const schema = Schema.Struct({ a: Schema.String }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { onExcessProperty: "preserve" } }) const sym = Symbol("sym") await decoding.succeed( { a: "a", b: "b", c: "c", [sym]: "sym" } ) }) }) it("should corectly handle __proto__", async () => { const schema = Schema.Struct({ ["__proto__"]: Schema.String }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ ["__proto__"]: "a" }) await decoding.fail( { __proto__: "a" }, `Missing key at ["__proto__"]` ) }) it(`{ readonly "a": string }`, async () => { const schema = Schema.Struct({ a: Schema.String }) const asserts = new TestSchema.Asserts(schema) // Should be able to access the fields deepStrictEqual(schema.fields, { a: Schema.String }) const make = asserts.make() await make.succeed({ a: "a" }) await make.fail(null, `Expected object, got null`) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.fail( {}, `Missing key at ["a"]` ) await decoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.fail( {}, `Missing key at ["a"]` ) await encoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) }) it(`{ readonly "a": }`, async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.fail( { a: "a" }, `Expected a finite number, got NaN at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.fail( { a: "a" }, `Expected number, got "a" at ["a"]` ) }) it("should use the identifier annotation as the expected message", async () => { const schema = Schema.Struct({ a: Schema.String }).annotate({ identifier: "ID" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail(null, `Expected ID, got null`) }) it(`Schema.optionalKey: { readonly "a"?: string }`, async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: "a" }) await make.succeed({}) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.succeed({}) await decoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.succeed({}) await encoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) }) it(`Schema.optional: { readonly "a"?: string | undefined }`, async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.String) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: "a" }) await make.succeed({ a: undefined }) await make.succeed({}) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.succeed({ a: undefined }) await decoding.succeed({}) await decoding.fail( { a: 1 }, `Expected string | undefined, got 1 at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.succeed({ a: undefined }) await encoding.succeed({}) await encoding.fail( { a: 1 }, `Expected string | undefined, got 1 at ["a"]` ) }) it(`{ readonly "a"?: }`, async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.fail( { a: undefined }, `Expected string, got undefined at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) }) describe("ParseOptions", () => { it(`{ errors: "all" }`, async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make({ parseOptions: { errors: "all" } }) await make.fail( {}, `Missing key at ["a"] Missing key at ["b"]` ) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( {}, `Missing key at ["a"] Missing key at ["b"]` ) const encoding = asserts.encoding({ parseOptions: { errors: "all" } }) await encoding.fail( {}, `Missing key at ["a"] Missing key at ["b"]` ) }) }) describe("assign", () => { it("Struct", async () => { const from = Schema.Struct({ a: Schema.String }) const schema = from.mapFields(Struct.assign({ b: Schema.String })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: "b" }) await decoding.fail( { b: "b" }, `Missing key at ["a"]` ) await decoding.fail( { a: "a" }, `Missing key at ["b"]` ) }) it("overlapping fields", async () => { const from = Schema.Struct({ a: Schema.String, b: Schema.String }) const schema = from.mapFields(Struct.assign({ b: Schema.Number, c: Schema.Number })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 1, c: 2 }) await decoding.fail( { a: "a", b: "b" }, `Expected number, got "b" at ["b"]` ) }) it("Struct & check & unsafePreserveChecks: true", async () => { const from = Schema.Struct({ a: Schema.String, b: Schema.String }).check( Schema.makeFilter(({ a, b }) => a === b, { expected: "a === b" }) ) const schema = from.mapFields(Struct.assign({ c: Schema.String }), { unsafePreserveChecks: true }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: "a", c: "c" }) await decoding.fail( { a: "", b: "b", c: "c" }, `Expected a === b, got {"a":"","b":"b","c":"c"}` ) }) }) describe("pick", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.String }).mapFields(Struct.pick(["a"])) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) }) }) describe("omit", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.String }).mapFields(Struct.omit(["b"])) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) }) }) }) describe("Tuple", () => { it("A required element cannot follow an optional element", () => { throws( () => Schema.Tuple([Schema.optionalKey(Schema.String), Schema.String]), new Error("A required element cannot follow an optional element. ts(1257)") ) throws( () => Schema.Tuple([Schema.optional(Schema.String), Schema.String]), new Error("A required element cannot follow an optional element. ts(1257)") ) }) it("should fail on unexpected indexes", async () => { const schema = Schema.Tuple([Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( ["a", "b"], `Unexpected key with value "b" at [1]` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( ["a", "b", "c"], `Unexpected key with value "b" at [1] Unexpected key with value "c" at [2]` ) }) it(`readonly [string]`, async () => { const schema = Schema.Tuple([Schema.NonEmptyString]) const asserts = new TestSchema.Asserts(schema) // should be able to access the elements deepStrictEqual(schema.elements, [Schema.NonEmptyString]) const make = asserts.make() await make.succeed(["a"]) await make.fail( [""], `Expected a value with a length of at least 1, got "" at [0]` ) const decoding = asserts.decoding() await decoding.succeed(["a"]) await decoding.fail(null, `Expected array, got null`) await decoding.fail( [], `Missing key at [0]` ) await decoding.fail( [1], `Expected string, got 1 at [0]` ) const encoding = asserts.encoding() await encoding.succeed(["a"]) await encoding.fail( [], `Missing key at [0]` ) await encoding.fail( [1], `Expected string, got 1 at [0]` ) }) it(`readonly [string?]`, async () => { const schema = Schema.Tuple([Schema.String.pipe(Schema.optionalKey)]) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(["a"]) await make.succeed([]) const decoding = asserts.decoding() await decoding.succeed(["a"]) await decoding.succeed([]) const encoding = asserts.encoding() await encoding.succeed(["a"]) await encoding.succeed([]) }) }) describe("Array", () => { it("should expose the element schema via .value", () => { const schema = Schema.Array(Schema.String) strictEqual(schema.value, Schema.String) strictEqual(schema.annotate({}).value, Schema.String) }) it("readonly string[]", async () => { const schema = Schema.Array(Schema.String) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(["a", "b"]) const decoding = asserts.decoding() await decoding.succeed(["a", "b"]) await decoding.fail( ["a", 1], `Expected string, got 1 at [1]` ) const encoding = asserts.encoding() await encoding.succeed(["a", "b"]) await encoding.fail( ["a", 1], `Expected string, got 1 at [1]` ) }) }) it("ArrayEnsure", async () => { const schema = Schema.ArrayEnsure(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("1", [1]) await decoding.succeed(["1", "2"], [1, 2]) await decoding.succeed([], []) await decoding.fail(null, `Expected string | array, got null`) await decoding.fail("a", `Expected a finite number, got NaN`) await decoding.fail( ["a"], `Expected a finite number, got NaN at [0]` ) const encoding = asserts.encoding() await encoding.succeed([], []) await encoding.succeed([1], "1") await encoding.succeed([1, 2], ["1", "2"]) }) describe("NonEmptyArray", () => { it("should expose the element schema via .value", () => { const schema = Schema.NonEmptyArray(Schema.String) strictEqual(schema.value, Schema.String) }) it("readonly string[]", async () => { const schema = Schema.NonEmptyArray(Schema.String) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed(["a"]) await make.succeed(["a", "b"]) const decoding = asserts.decoding() await decoding.succeed(["a"]) await decoding.succeed(["a", "b"]) await decoding.fail( [], `Missing key at [0]` ) await decoding.fail( ["a", 1], `Expected string, got 1 at [1]` ) const encoding = asserts.encoding() await encoding.succeed(["a"]) await encoding.succeed(["a", "b"]) await encoding.fail( [], `Missing key at [0]` ) await encoding.fail( ["a", 1], `Expected string, got 1 at [1]` ) }) }) it("Trimmed", async () => { const schema = Schema.Trimmed const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( " a ", `Expected a string with no leading or trailing whitespace, got " a "` ) }) describe("Checks", () => { describe("check", () => { it("single check", async () => { const schema = Schema.String.check(Schema.isMinLength(3)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("abc") await decoding.fail( "ab", `Expected a value with a length of at least 3, got "ab"` ) }) it("multiple checks", async () => { const schema = Schema.String.check( Schema.isMinLength(3), Schema.isIncludes("c") ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("abc") await decoding.fail( "ab", `Expected a value with a length of at least 3, got "ab"` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( "ab", `Expected a value with a length of at least 3, got "ab" Expected a string including "c", got "ab"` ) }) it("aborting checks", async () => { const schema = Schema.String.check( Schema.isMinLength(2).abort(), Schema.isIncludes("b") ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `Expected a value with a length of at least 2, got "a"` ) }) it("makeFilterGroup", async () => { const usernameGroup = Schema.makeFilterGroup( [ Schema.isMinLength(3), Schema.isPattern(/^[a-zA-Z0-9]+$/, { title: "alphanumeric", description: "must contain only letters and numbers" }), Schema.isTrimmed() ], { title: "username", description: "a valid username" } ) const Username = Schema.String.check(usernameGroup) const asserts = new TestSchema.Asserts(Username) const decoding = asserts.decoding() await decoding.succeed("abc") await decoding.fail( "", `Expected a value with a length of at least 3, got ""` ) }) it("object-level checks are validated against the decoded value when encoding", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString }).check( Schema.makeFilter((o) => typeof o.a === "number", { expected: "a is a number" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) }) it("suspended checks are not supported", () => { throws( () => { Schema.suspend(() => Schema.Struct({ a: Schema.FiniteFromString })).check( Schema.makeFilter((o) => typeof o.a === "number", { expected: "a is a number" }) ) }, "Cannot add checks to Suspend" ) }) }) it("refine", async () => { const schema = Schema.Option(Schema.String).pipe( Schema.refine(Option.isSome, { expected: "isSome" }), Schema.check( Schema.makeFilter(({ value }) => value.length > 0, { expected: "length > 0" }) ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(Option.some("a")) await decoding.fail( Option.some(""), `Expected length > 0, got some("")` ) await decoding.fail( Option.none(), `Expected isSome, got none()` ) }) describe("String checks", () => { it("isPattern", async () => { const schema = Schema.String.check(Schema.isPattern(/^a/)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "b", `Expected a string matching the RegExp ^a, got "b"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", `Expected a string matching the RegExp ^a, got "b"` ) }) it("isStartsWith", async () => { const schema = Schema.String.check(Schema.isStartsWith("a")) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "b", `Expected a string starting with "a", got "b"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", `Expected a string starting with "a", got "b"` ) }) it("isEndsWith", async () => { const schema = Schema.String.check(Schema.isEndsWith("a")) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "b", `Expected a string ending with "a", got "b"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", `Expected a string ending with "a", got "b"` ) }) it("isLowercased", async () => { const schema = Schema.String.check(Schema.isLowercased()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "A", `Expected a string with all characters in lowercase, got "A"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "A", `Expected a string with all characters in lowercase, got "A"` ) }) it("isUppercased", async () => { const schema = Schema.String.check(Schema.isUppercased()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("A") await decoding.fail( "a", `Expected a string with all characters in uppercase, got "a"` ) const encoding = asserts.encoding() await encoding.succeed("A") await encoding.fail( "a", `Expected a string with all characters in uppercase, got "a"` ) }) it("isCapitalized", async () => { const schema = Schema.String.check(Schema.isCapitalized()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("Abc") await decoding.fail( "abc", `Expected a string with the first character in uppercase, got "abc"` ) const encoding = asserts.encoding() await encoding.succeed("Abc") await encoding.fail( "abc", `Expected a string with the first character in uppercase, got "abc"` ) }) it("isUncapitalized", async () => { const schema = Schema.String.check(Schema.isUncapitalized()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("aBC") await decoding.fail( "ABC", `Expected a string with the first character in lowercase, got "ABC"` ) const encoding = asserts.encoding() await encoding.succeed("aBC") await encoding.fail( "ABC", `Expected a string with the first character in lowercase, got "ABC"` ) }) it("isMinLength", async () => { const schema = Schema.String.check(Schema.isMinLength(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "", `Expected a value with a length of at least 1, got ""` ) }) }) describe("Number checks", () => { it("isGreaterThan", async () => { const schema = Schema.Number.check(Schema.isGreaterThan(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(2) await decoding.fail( 1, `Expected a value greater than 1, got 1` ) const encoding = asserts.encoding() await encoding.succeed(2) await encoding.fail( 1, `Expected a value greater than 1, got 1` ) }) it("isGreaterThanOrEqualTo", async () => { const schema = Schema.Number.check(Schema.isGreaterThanOrEqualTo(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 0, `Expected a value greater than or equal to 1, got 0` ) }) it("isLessThan", async () => { const schema = Schema.Number.check(Schema.isLessThan(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(0) await decoding.fail( 1, `Expected a value less than 1, got 1` ) }) it("isLessThanOrEqualTo", async () => { const schema = Schema.Number.check(Schema.isLessThanOrEqualTo(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 2, `Expected a value less than or equal to 1, got 2` ) }) it("isMultipleOf", async () => { const schema = Schema.Number.check(Schema.isMultipleOf(2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(4) await decoding.fail( 3, `Expected a value that is a multiple of 2, got 3` ) }) it("isMultipleOf rejects nonzero subnormal remainders", () => { const is = Schema.is(Schema.Number.check(Schema.isMultipleOf(Number("1e-323")))) assertFalse(is(Number("1.042e-321"))) }) describe("isBetween", () => { it("included & included", async () => { const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3 })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.succeed(3) await decoding.fail( 0, `Expected a value between 1 and 3, got 0` ) await decoding.fail( 4, `Expected a value between 1 and 3, got 4` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.succeed(3) await encoding.fail( 0, `Expected a value between 1 and 3, got 0` ) }) it("included & excluded", async () => { const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMaximum: true })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`) await decoding.fail( 0, `Expected a value between 1 and 3 (excluded), got 0` ) await decoding.fail( 4, `Expected a value between 1 and 3 (excluded), got 4` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail(3, `Expected a value between 1 and 3 (excluded), got 3`) await encoding.fail( 0, `Expected a value between 1 and 3 (excluded), got 0` ) }) it("excluded & included", async () => { const schema = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMinimum: true })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`) await decoding.succeed(3) await decoding.fail( 0, `Expected a value between 1 (excluded) and 3, got 0` ) await decoding.fail( 4, `Expected a value between 1 (excluded) and 3, got 4` ) const encoding = asserts.encoding() await encoding.fail(1, `Expected a value between 1 (excluded) and 3, got 1`) await encoding.succeed(3) await encoding.fail( 0, `Expected a value between 1 (excluded) and 3, got 0` ) }) it("excluded & excluded", async () => { const schema = Schema.Int.check( Schema.isBetween({ minimum: 1, maximum: 3, exclusiveMinimum: true, exclusiveMaximum: true }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(2) await decoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`) await decoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`) await decoding.fail( 0, `Expected a value between 1 (excluded) and 3 (excluded), got 0` ) await decoding.fail( 4, `Expected a value between 1 (excluded) and 3 (excluded), got 4` ) const encoding = asserts.encoding() await encoding.succeed(2) await encoding.fail(1, `Expected a value between 1 (excluded) and 3 (excluded), got 1`) await encoding.fail(3, `Expected a value between 1 (excluded) and 3 (excluded), got 3`) await encoding.fail( 0, `Expected a value between 1 (excluded) and 3 (excluded), got 0` ) }) }) it("isInt", async () => { const schema = Schema.Number.check(Schema.isInt()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 1.1, `Expected an integer, got 1.1` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, `Expected an integer, got 1.1` ) await decoding.fail( NaN, `Expected an integer, got NaN` ) await decoding.fail( Infinity, `Expected an integer, got Infinity` ) await decoding.fail( -Infinity, `Expected an integer, got -Infinity` ) }) it("isInt32", async () => { const schema = Schema.Number.check(Schema.isInt32()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 1.1, `Expected an integer, got 1.1` ) await decoding.fail( Number.MAX_SAFE_INTEGER + 1, `Expected an integer, got 9007199254740992` ) await decoding.fail( 1.1, `Expected an integer, got 1.1` ) await decoding.fail( Number.MIN_SAFE_INTEGER - 1, `Expected an integer, got -9007199254740992` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( Number.MAX_SAFE_INTEGER + 1, `Expected an integer, got 9007199254740992 Expected a value between -2147483648 and 2147483647, got 9007199254740992` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, `Expected an integer, got 1.1` ) await encoding.fail( Number.MAX_SAFE_INTEGER + 1, `Expected an integer, got 9007199254740992` ) }) }) describe("BigInt Checks", () => { const options = { order: Order.BigInt, format: (value: bigint) => `${value}n` } const isBetween = Schema.makeIsBetween(options) const isGreaterThan = Schema.makeIsGreaterThan(options) const isGreaterThanOrEqualTo = Schema.makeIsGreaterThanOrEqualTo(options) const isLessThan = Schema.makeIsLessThan(options) const isLessThanOrEqualTo = Schema.makeIsLessThanOrEqualTo(options) it("isBetween", async () => { const schema = Schema.BigInt.check(isBetween({ minimum: 5n, maximum: 10n })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(5n) await decoding.succeed(7n) await decoding.succeed(10n) await decoding.fail( 4n, `Expected a value between 5n and 10n, got 4n` ) }) it("isGreaterThan", async () => { const schema = Schema.BigInt.check(isGreaterThan(5n)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(6n) await decoding.fail( 5n, `Expected a value greater than 5n, got 5n` ) }) it("isGreaterThanOrEqualTo", async () => { const schema = Schema.BigInt.check(isGreaterThanOrEqualTo(5n)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(5n) await decoding.succeed(6n) await decoding.fail( 4n, `Expected a value greater than or equal to 5n, got 4n` ) }) it("isLessThan", async () => { const schema = Schema.BigInt.check(isLessThan(5n)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(4n) await decoding.fail( 5n, `Expected a value less than 5n, got 5n` ) }) it("isLessThanOrEqualTo", async () => { const schema = Schema.BigInt.check(isLessThanOrEqualTo(5n)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(5n) await decoding.succeed(4n) await decoding.fail( 6n, `Expected a value less than or equal to 5n, got 6n` ) }) }) describe("Record checks", () => { it("isMinProperties", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMinProperties(1)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( {}, `Expected a value with at least 1 entry, got {}` ) }) it("isMaxProperties", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 1, b: 2, c: 3 }, `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}` ) await decoding.fail( { a: 1, b: 2, c: 3 }, `Expected a value with at most 2 entries, got {"a":1,"b":2,"c":3}` ) }) it("isMinProperties with symbol keys", async () => { const sym = Symbol("test") const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Finite).check( Schema.isMinProperties(2) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, [sym]: 2 }) await decoding.fail( { [sym]: 1 }, `Expected a value with at least 2 entries, got {Symbol(test):1}` ) }) it("isMaxProperties with symbol keys", async () => { const sym1 = Symbol("test1") const sym2 = Symbol("test2") const sym3 = Symbol("test3") const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Finite).check( Schema.isMaxProperties(2) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ [sym1]: 1, [sym2]: 2 }) await decoding.fail( { [sym1]: 1, [sym2]: 2, [sym3]: 3 }, `Expected a value with at most 2 entries, got {Symbol(test1):1,Symbol(test2):2,Symbol(test3):3}` ) }) it("isPropertiesLengthBetween", async () => { const schema = Schema.Record(Schema.String, Schema.Number).check(Schema.isPropertiesLengthBetween(2, 2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, b: 2 }) await decoding.succeed({ ["__proto__"]: 0, "": 0 }) await decoding.fail( { a: 1 }, `Expected a value with exactly 2 entries, got {"a":1}` ) await decoding.fail( { a: 1, b: 2, c: 3 }, `Expected a value with exactly 2 entries, got {"a":1,"b":2,"c":3}` ) }) it("isPropertiesLengthBetween with symbol keys", async () => { const sym1 = Symbol("test1") const sym2 = Symbol("test2") const schema = Schema.Record(Schema.Union([Schema.String, Schema.Symbol]), Schema.Number).check( Schema.isPropertiesLengthBetween(2, 2) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ [sym1]: 1, [sym2]: 2 }) await decoding.succeed({ a: 1, [sym1]: 2 }) await decoding.fail( { [sym1]: 1 }, `Expected a value with exactly 2 entries, got {Symbol(test1):1}` ) await decoding.fail( { [sym1]: 1, [sym2]: 2, a: 3 }, `Expected a value with exactly 2 entries, got {"a":3,Symbol(test1):1,Symbol(test2):2}` ) }) it("isPropertyNames", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check( Schema.isPropertyNames(Schema.String.check(Schema.isPattern(/^[A-Z]/))) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ Ab: 1 }) await decoding.fail( { ab: 1 }, `Expected a string matching the RegExp ^[A-Z], got "ab" at ["ab"]` ) }) it("isPropertyNames with Never", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isPropertyNames(Schema.Never)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}) await decoding.fail( { a: 1 }, `Expected never, got "a" at ["a"]` ) }) }) describe("Structural checks", () => { it("Array + isMinLength", async () => { const schema = Schema.Struct({ tags: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(3)) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( {}, `Missing key at ["tags"]` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( { tags: ["a", ""] }, `Expected a value with a length of at least 1, got "" at ["tags"][1] Expected a value with a length of at least 3, got ["a",""] at ["tags"]` ) }) it("Record + isMaxProperties", async () => { const schema = Schema.Record(Schema.String, Schema.Finite).check(Schema.isMaxProperties(2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( null, `Expected object, got null` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( { a: 1, b: NaN, c: 3 }, `Expected a finite number, got NaN at ["b"] Expected a value with at most 2 entries, got {"a":1,"b":NaN,"c":3}` ) }) it("ReadonlyMap + isMaxSize", async () => { const schema = Schema.ReadonlyMap(Schema.String, Schema.Finite).check(Schema.isMaxSize(2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( null, `Expected ReadonlyMap, got null` ) const decodingAll = asserts.decoding({ parseOptions: { errors: "all" } }) await decodingAll.fail( new Map([["a", 1], ["b", NaN], ["c", 3]]), `Expected a finite number, got NaN at ["entries"][1][1] Expected a value with a size of at most 2, got Map([["a",1],["b",NaN],["c",3]])` ) }) }) describe("Array checks", () => { it("UniqueArray", async () => { const schema = Schema.UniqueArray(Schema.Struct({ a: Schema.String, b: Schema.String })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed([{ a: "a", b: "b" }, { a: "c", b: "d" }]) await decoding.fail( [{ a: "a", b: "b" }, { a: "a", b: "b" }], `Expected an array with unique items, got [{"a":"a","b":"b"},{"a":"a","b":"b"}]` ) }) }) }) it("Finite", async () => { const schema = Schema.Finite const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) describe("Transformations", () => { it("NumberFromString", async () => { const schema = Schema.NumberFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.succeed("NaN", NaN) await decoding.succeed("Infinity", Infinity) await decoding.succeed("+Infinity", Infinity) await decoding.succeed("-Infinity", -Infinity) const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.succeed(NaN, "NaN") await encoding.succeed(Infinity, "Infinity") await encoding.succeed(-Infinity, "-Infinity") await encoding.fail( "a", `Expected number, got "a"` ) }) it("DateFromString", async () => { const schema = Schema.DateFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("2021-01-01T00:00:00.000Z", new Date("2021-01-01T00:00:00.000Z")) const encoding = asserts.encoding() await encoding.succeed(new Date("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z") }) it("DateFromMillis", async () => { const schema = Schema.DateFromMillis const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(0, new Date(0)) assertTrue(Schema.decodeSync(schema)(NaN) instanceof Date) assertTrue(Schema.decodeSync(schema)(Infinity) instanceof Date) assertTrue(Schema.decodeSync(schema)(-Infinity) instanceof Date) await decoding.fail(null, `Expected number, got null`) const encoding = asserts.encoding() await encoding.succeed(new Date(0), 0) strictEqual(Schema.encodeSync(schema)(new Date("invalid")), NaN) strictEqual(Schema.encodeSync(schema)(new Date(NaN)), NaN) strictEqual(Schema.encodeSync(schema)(new Date(Infinity)), NaN) strictEqual(Schema.encodeSync(schema)(new Date(-Infinity)), NaN) }) it("FiniteFromString", async () => { const schema = Schema.FiniteFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.fail( "a", `Expected a finite number, got NaN` ) await decoding.fail( "NaN", `Expected a finite number, got NaN` ) await decoding.fail( "Infintiy", `Expected a finite number, got NaN` ) await decoding.fail( "+Infintiy", `Expected a finite number, got NaN` ) await decoding.fail( "-Infintiy", `Expected a finite number, got NaN` ) const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.fail( "a", `Expected number, got "a"` ) }) it("BigIntFromString", async () => { const schema = Schema.BigIntFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("0", 0n) await decoding.fail( "a", `Expected a string representing a bigint, got "a"` ) const encoding = asserts.encoding() await encoding.succeed(0n, "0") }) it("BigDecimalFromString", async () => { const schema = Schema.BigDecimalFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("0", Option.getOrThrow(BigDecimal.fromString("0"))) await decoding.succeed("123.456", Option.getOrThrow(BigDecimal.fromString("123.456"))) const encoding = asserts.encoding() await encoding.succeed(BigDecimal.make(0n, 0), "0") await encoding.succeed(BigDecimal.make(123456n, 3), "123.456") await encoding.fail( "a", `Expected BigDecimal, got "a"` ) }) it("TimeZoneNamedFromString", async () => { const schema = Schema.TimeZoneNamedFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("Europe/London", DateTime.zoneMakeNamedUnsafe("Europe/London")) const encoding = asserts.encoding() await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"), "Europe/London") await encoding.fail( "a", `Expected DateTime.TimeZone.Named, got "a"` ) }) it("TimeZoneFromString", async () => { const schema = Schema.TimeZoneFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("Europe/London", DateTime.zoneMakeNamedUnsafe("Europe/London")) await decoding.succeed("+03:00", DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) const encoding = asserts.encoding() await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London"), "Europe/London") await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000), "+03:00") await encoding.fail( "a", `Expected DateTime.TimeZone, got "a"` ) }) it("DateTimeZonedFromString", async () => { const schema = Schema.DateTimeZonedFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const zoned = DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" }) const decoding = asserts.decoding() await decoding.fail("invalid", `Invalid Zoned DateTime string: invalid`) const encoding = asserts.encoding() await encoding.succeed(zoned, DateTime.formatIsoZoned(zoned)) await encoding.fail( "a", `Expected DateTime.Zoned, got "a"` ) }) it("FiniteFromString & isGreaterThan", async () => { const schema = Schema.FiniteFromString.check(Schema.isGreaterThan(2)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("3", 3) await decoding.fail( "1", `Expected a value greater than 2, got 1` ) const encoding = asserts.encoding() await encoding.succeed(3, "3") await encoding.fail( 1, `Expected a value greater than 2, got 1` ) }) }) describe("decodeTo", () => { it("should expose the source and the target schemas", () => { const schema = Schema.FiniteFromString strictEqual(schema.from.ast._tag, "String") strictEqual(schema.to, Schema.Finite) }) it("required to required", async () => { const schema = Schema.Struct({ a: Schema.String.pipe( Schema.decodeTo( Schema.String, SchemaTransformation.passthrough() ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.fail( {}, `Missing key at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.fail( {}, `Missing key at ["a"]` ) }) it("required to optional", async () => { const schema = Schema.Struct({ a: Schema.String.pipe( Schema.decodeTo( Schema.optionalKey(Schema.String), { decode: SchemaGetter.required(), encode: SchemaGetter.withDefault(Effect.succeed("default")) } ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.fail( {}, `Missing key at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.succeed({}, { a: "default" }) }) it("optionalKey to required", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String).pipe( Schema.decodeTo( Schema.String, { decode: SchemaGetter.withDefault(Effect.succeed("default")), encode: SchemaGetter.passthrough() } ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.succeed({}, { a: "default" }) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) }) it("double transformation", async () => { const schema = Schema.Trim.pipe(Schema.decodeTo( Schema.FiniteFromString, SchemaTransformation.passthrough() )) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(" 2 ", 2) await decoding.fail( " a2 ", `Expected a finite number, got NaN` ) const encoding = asserts.encoding() await encoding.succeed(2, "2") }) it("double transformation with checks", async () => { const schema = Schema.Struct({ a: Schema.String.check(Schema.isMinLength(2)).pipe( Schema.decodeTo( Schema.String.check(Schema.isMinLength(3)), SchemaTransformation.passthrough() ), Schema.decodeTo( Schema.String, SchemaTransformation.passthrough() ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "aaa" }) await decoding.fail( { a: "aa" }, `Expected a value with a length of at least 3, got "aa" at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "aaa" }) await encoding.fail( { a: "aa" }, `Expected a value with a length of at least 3, got "aa" at ["a"]` ) }) it("nested defaults", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.Struct({ b: Schema.optionalKey(Schema.String) })).pipe(Schema.decodeTo( Schema.Struct({ b: Schema.optionalKey(Schema.String).pipe( Schema.decodeTo( Schema.String, { decode: SchemaGetter.withDefault(Effect.succeed("default-b")), encode: SchemaGetter.passthrough() } ) ) }), { decode: SchemaGetter.withDefault(Effect.succeed({})), encode: SchemaGetter.passthrough() } )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: { b: "b" } }) await decoding.succeed({ a: {} }, { a: { b: "default-b" } }) await decoding.succeed({}, { a: { b: "default-b" } }) }) }) describe("decode", () => { it("double transformation", async () => { const schema = Schema.String.pipe( Schema.decode( SchemaTransformation.trim().compose( SchemaTransformation.toLowerCase() ) ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(" A ", "a") const encoding = asserts.encoding() await encoding.succeed(" A ", " A ") }) }) describe("encodeTo", () => { it("required to required", async () => { const schema = Schema.Struct({ a: Schema.String.pipe( Schema.encodeTo( Schema.String, SchemaTransformation.passthrough() ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.fail( {}, `Missing key at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.fail( {}, `Missing key at ["a"]` ) }) it("required to optionalKey", async () => { const schema = Schema.Struct({ a: Schema.String.pipe( Schema.encodeTo( Schema.optionalKey(Schema.String), { decode: SchemaGetter.withDefault(Effect.succeed("default")), encode: SchemaGetter.passthrough() } ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.succeed({}, { a: "default" }) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) }) it("optionalKey to required", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String).pipe( Schema.encodeTo( Schema.String, { decode: SchemaGetter.required(), encode: SchemaGetter.withDefault(Effect.succeed("default")) } ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.fail( {}, `Missing key at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "a" }) await encoding.succeed({}, { a: "default" }) }) it("double transformation", async () => { const schema = Schema.FiniteFromString.pipe(Schema.encodeTo( Schema.Trim, SchemaTransformation.passthrough() )) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(" 2 ", 2) await decoding.fail( " a2 ", `Expected a finite number, got NaN` ) const encoding = asserts.encoding() await encoding.succeed(2, "2") }) it("double transformation with checks", async () => { const schema = Schema.Struct({ a: Schema.String.pipe( Schema.encodeTo( Schema.String.check(Schema.isMinLength(3)), SchemaTransformation.passthrough() ), Schema.encodeTo( Schema.String.check(Schema.isMinLength(2)), SchemaTransformation.passthrough() ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "aaa" }) await decoding.fail( { a: "aa" }, `Expected a value with a length of at least 3, got "aa" at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: "aaa" }) await encoding.fail( { a: "aa" }, `Expected a value with a length of at least 3, got "aa" at ["a"]` ) }) }) describe("encode", () => { it("double transformation", async () => { const schema = Schema.String.pipe( Schema.encode( SchemaTransformation.trim().compose( SchemaTransformation.toLowerCase() ).flip() ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(" A ", " A ") const encoding = asserts.encoding() await encoding.succeed(" A ", "a") }) }) describe("flip", () => { it("should expose the schema", () => { const schema = Schema.Struct({ a: Schema.String }) const flipped = schema.pipe(Schema.flip) strictEqual(flipped.schema, schema) }) it("string & isMinLength(3) <-> number & isGreaterThan(2)", async () => { const schema = Schema.FiniteFromString.pipe( Schema.check(Schema.isGreaterThan(2)), Schema.flip, Schema.check(Schema.isMinLength(3)) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( 2, `Expected a value greater than 2, got 2` ) await decoding.fail( 3, `Expected a value with a length of at least 3, got "3"` ) const encoding = asserts.encoding() await encoding.succeed("123", 123) }) it("Struct & flip & check & flip should apply the check to the encoded side", async () => { const schema = Schema.Struct({ a: Schema.String }).pipe( Schema.flip, Schema.check(Schema.makeFilter((o) => o.a.length > 1, { expected: "a length > 1" })), Schema.flip ) assertTrue(SchemaAST.isObjects(schema.ast)) strictEqual(schema.ast.checks, undefined) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( { a: "a" }, `Expected a length > 1, got {"a":"a"}` ) await decoding.succeed({ a: "aa" }) const encoding = asserts.encoding() await encoding.fail( { a: "a" }, `Expected a length > 1, got {"a":"a"}` ) await encoding.succeed({ a: "aa" }) }) it("Tuple & flip & check & flip should apply the check to the encoded side", async () => { const schema = Schema.Tuple([Schema.String]).pipe( Schema.flip, Schema.check(Schema.makeFilter((tuple) => tuple[0].length > 1, { expected: "head length > 1" })), Schema.flip ) assertTrue(SchemaAST.isArrays(schema.ast)) strictEqual(schema.ast.checks, undefined) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( ["a"], `Expected head length > 1, got ["a"]` ) await decoding.succeed(["aa"]) const encoding = asserts.encoding() await encoding.fail( ["a"], `Expected head length > 1, got ["a"]` ) await encoding.succeed(["aa"]) }) it("Union & flip & check & flip should apply the check to the encoded side", async () => { const schema = Schema.Union([Schema.Literal("a"), Schema.Literal("aa")]).pipe( Schema.flip, Schema.check(Schema.makeFilter((s) => s === "aa", { expected: `"aa"` })), Schema.flip ) assertTrue(SchemaAST.isUnion(schema.ast)) strictEqual(schema.ast.checks, undefined) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `Expected "aa", got "a"` ) await decoding.succeed("aa") const encoding = asserts.encoding() await encoding.fail( "a", `Expected "aa", got "a"` ) await encoding.succeed("aa") }) it("Declaration & flip & check & flip should apply the check to the encoded side", async () => { const schema = Schema.declare( (u): u is string => typeof u === "string", { expected: "string declaration" } ).pipe( Schema.flip, Schema.check(Schema.makeFilter((s) => s.length > 1, { expected: "a length > 1" })), Schema.flip ) assertTrue(SchemaAST.isDeclaration(schema.ast)) strictEqual(schema.ast.checks, undefined) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `Expected a length > 1, got "a"` ) await decoding.succeed("aa") const encoding = asserts.encoding() await encoding.fail( "a", `Expected a length > 1, got "a"` ) await encoding.succeed("aa") }) it("Struct & flip & check & flip with encoding chain should check the local value", async () => { const local = Schema.Struct({ a: Schema.String }).pipe( Schema.flip, Schema.check(Schema.makeFilter((o) => typeof o.a === "string" && o.a.length > 1, { expected: "a length > 1" })), Schema.flip ) const schema = Schema.Struct({ b: Schema.String }).pipe( Schema.decodeTo(local, { decode: SchemaGetter.transform<{ readonly a: string }, { readonly b: string }>((o) => ({ a: o.b })), encode: SchemaGetter.transform<{ readonly b: string }, { readonly a: string }>((o) => ({ b: o.a })) }) ) assertTrue(SchemaAST.isObjects(schema.ast)) strictEqual(schema.ast.encoding?.length, 1) strictEqual(schema.ast.encodingChecks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( { b: "a" }, `Expected a length > 1, got {"a":"a"}` ) await decoding.succeed({ b: "aa" }, { a: "aa" }) const encoding = asserts.encoding() await encoding.fail( { a: "a" }, `Expected a length > 1, got {"a":"a"}` ) await encoding.succeed({ a: "aa" }, { b: "aa" }) }) it(`Struct & encoding chain & structural checks should check the local value with errors: "all"`, async () => { const local = Schema.Struct({ a: Schema.Finite }).check(Schema.isMaxProperties(1)) const schema = Schema.Struct({ b: Schema.Number, c: Schema.String }).pipe( Schema.decodeTo(local, { decode: SchemaGetter.transform< { readonly a: number }, { readonly b: number; readonly c: string } >((o) => ({ a: o.b })), encode: SchemaGetter.transform< { readonly b: number; readonly c: string }, { readonly a: number } >((o) => ({ b: o.a, c: "" })) }) ) assertTrue(SchemaAST.isObjects(schema.ast)) strictEqual(schema.ast.encoding?.length, 1) strictEqual(schema.ast.checks?.length, 1) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( { b: NaN, c: "extra" }, `Expected a finite number, got NaN at ["a"]` ) const encoding = asserts.encoding({ parseOptions: { errors: "all" } }) await encoding.fail( { a: NaN }, `Expected a finite number, got NaN at ["a"]` ) await encoding.succeed({ a: 1 }, { b: 1, c: "" }) }) it("should work with withConstructorDefault", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({}, { a: -1 }) const flipped = schema.pipe(Schema.flip) const assertsFlipped = new TestSchema.Asserts(flipped) throws(() => flipped.make({} as any)) const makeFlipped = assertsFlipped.make() await makeFlipped.succeed({ a: "1" }) const flipped2 = flipped.pipe(Schema.flip) const assertsFlipped2 = new TestSchema.Asserts(flipped2) deepStrictEqual(flipped2.fields, schema.fields) const makeFlipped2 = assertsFlipped2.make() await makeFlipped2.succeed({ a: 1 }) await makeFlipped2.succeed({}, { a: -1 }) }) }) it("declare", async () => { const schema = Schema.declare( (u) => u instanceof File, { expected: "File" } ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(new File([], "a.txt")) await decoding.fail("a", `Expected File, got "a"`) }) describe("Redacted", () => { it("should expose the value", () => { const schema = Schema.Redacted(Schema.String) strictEqual(schema.value, Schema.String) strictEqual(schema.annotate({}).value, Schema.String) }) it("Redacted(Finite)", async () => { const schema = Schema.Redacted(Schema.Int) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Redacted.make(123)) await decoding.fail(null, `Expected Redacted, got null`) await decoding.fail( Redacted.make("a"), `Invalid data at ["value"]` ) await decoding.fail( Redacted.make(1.2), `Invalid data at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Redacted.make(123)) await encoding.fail(null, `Expected Redacted, got null`) await encoding.fail( Redacted.make("a"), `Invalid data at ["value"]` ) await encoding.fail( Redacted.make(1.2), `Invalid data at ["value"]` ) }) it("Redacted(FiniteFromString)", async () => { const schema = Schema.Redacted(Schema.FiniteFromString.check(Schema.isInt())) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Redacted.make("123"), Redacted.make(123)) await decoding.fail(null, `Expected Redacted, got null`) await decoding.fail( Redacted.make(null), `Invalid data at ["value"]` ) await decoding.fail( Redacted.make("a"), `Invalid data at ["value"]` ) await decoding.fail( Redacted.make("1.2"), `Invalid data at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Redacted.make(123), Redacted.make("123")) await encoding.fail(null, `Expected Redacted, got null`) await encoding.fail( Redacted.make(null), `Invalid data at ["value"]` ) await encoding.fail( Redacted.make("a"), `Invalid data at ["value"]` ) await encoding.fail( Redacted.make(1.2), `Invalid data at ["value"]` ) }) it("with label", async () => { const schema = Schema.Redacted(Schema.NonEmptyString, { label: "password" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(Redacted.make("a", { label: "password" })) await decoding.fail( Redacted.make("a", { label: "API key" }), `Expected "password", got "API key" at ["label"]` ) await decoding.fail( Redacted.make(1, { label: "API key" }), `Expected "password", got "API key" at ["label"]` ) await decoding.fail( Redacted.make(1, { label: "password" }), `Invalid data at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Redacted.make("a", { label: "password" })) await encoding.fail( Redacted.make("a", { label: "API key" }), `Expected "password", got "API key" at ["label"]` ) await encoding.fail( Redacted.make("", { label: "password" }), `Invalid data at ["value"]` ) }) }) describe("RedactedFromValue", () => { it("should not leak any information about the value", async () => { const schema = Schema.RedactedFromValue(Schema.Literal("secret")) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail(null, `Invalid data `) }) it("should decode a value", async () => { const schema = Schema.RedactedFromValue(Schema.FiniteFromString.check(Schema.isInt())) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("123", Redacted.make(123)) await decoding.fail(null, `Invalid data `) await decoding.fail("1.2", `Invalid data `) }) }) describe("Option", () => { it("should expose the value", () => { const schema = Schema.Option(Schema.String) strictEqual(schema.value, Schema.String) strictEqual(schema.annotate({}).value, Schema.String) }) it("Option(FiniteFromString)", async () => { const schema = Schema.Option(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Option.none()) await decoding.succeed(Option.some("123"), Option.some(123)) await decoding.fail(null, `Expected Option, got null`) await decoding.fail( Option.some(null), `Expected string, got null at ["value"]` ) const encoding = asserts.encoding() await encoding.succeed(Option.none()) await encoding.succeed(Option.some(123), Option.some("123")) await encoding.fail(null, `Expected Option, got null`) await encoding.fail( Option.some(null), `Expected number, got null at ["value"]` ) }) }) it("OptionFromNullOr", async () => { const schema = Schema.OptionFromNullOr(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(null, Option.none()) await decoding.succeed("1", Option.some(1)) await decoding.fail("a", `Expected a finite number, got NaN`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), null) await encoding.succeed(Option.some(1), "1") }) it("OptionFromUndefinedOr", async () => { const schema = Schema.OptionFromUndefinedOr(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) await decoding.fail("a", `Expected a finite number, got NaN`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), undefined) await encoding.succeed(Option.some(1), "1") }) describe("OptionFromNullishOr", () => { it("onNoneEncoding: null", async () => { const schema = Schema.OptionFromNullishOr(Schema.FiniteFromString, { onNoneEncoding: null }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(null, Option.none()) await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) await decoding.fail("a", `Expected a finite number, got NaN`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), null) await encoding.succeed(Option.some(1), "1") }) it("onNoneEncoding: undefined", async () => { const schema = Schema.OptionFromNullishOr(Schema.FiniteFromString, { onNoneEncoding: undefined }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(null, Option.none()) await decoding.succeed(undefined, Option.none()) await decoding.succeed("1", Option.some(1)) await decoding.fail("a", `Expected a finite number, got NaN`) const encoding = asserts.encoding() await encoding.succeed(Option.none(), undefined) await encoding.succeed(Option.some(1), "1") }) }) it("OptionFromOptionalKey", async () => { const schema = Schema.Struct({ a: Schema.OptionFromOptionalKey(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: undefined }, `Expected string, got undefined at ["a"]` ) await decoding.fail( { a: "a" }, `Expected a finite number, got NaN at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: Option.none() }, {}) await encoding.succeed({ a: Option.some(1) }, { a: "1" }) }) it("OptionFromOptional", async () => { const schema = Schema.Struct({ a: Schema.OptionFromOptional(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: undefined }, { a: Option.none() }) await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: "a" }, `Expected a finite number, got NaN at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: Option.none() }, {}) await encoding.succeed({ a: Option.some(1) }, { a: "1" }) }) describe("OptionFromOptionalNullOr", () => { it("default (omit)", async () => { const schema = Schema.Struct({ a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: null }, { a: Option.none() }) await decoding.succeed({ a: undefined }, { a: Option.none() }) await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.fail( { a: "a" }, `Expected a finite number, got NaN at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: Option.none() }, {}) await encoding.succeed({ a: Option.some(1) }, { a: "1" }) await encoding.fail( { a: null }, `Expected Option, got null at ["a"]` ) }) it("onNoneEncoding: null", async () => { const schema = Schema.Struct({ a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString, { onNoneEncoding: null }) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: null }, { a: Option.none() }) await decoding.succeed({ a: undefined }, { a: Option.none() }) await decoding.succeed({ a: "1" }, { a: Option.some(1) }) const encoding = asserts.encoding() await encoding.succeed({ a: Option.none() }, { a: null }) await encoding.succeed({ a: Option.some(1) }, { a: "1" }) }) it("onNoneEncoding: undefined", async () => { const schema = Schema.Struct({ a: Schema.OptionFromOptionalNullOr(Schema.FiniteFromString, { onNoneEncoding: undefined }) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: null }, { a: Option.none() }) await decoding.succeed({ a: undefined }, { a: Option.none() }) await decoding.succeed({ a: "1" }, { a: Option.some(1) }) const encoding = asserts.encoding() await encoding.succeed({ a: Option.none() }, { a: undefined }) await encoding.succeed({ a: Option.some(1) }, { a: "1" }) }) }) describe("Result", () => { it("should expose the values", () => { const schema = Schema.Result(Schema.String, Schema.Number) strictEqual(schema.success, Schema.String) strictEqual(schema.annotate({}).success, Schema.String) strictEqual(schema.failure, Schema.Number) strictEqual(schema.annotate({}).failure, Schema.Number) }) it("Result(FiniteFromString, FiniteFromString)", async () => { const schema = Schema.Result(Schema.FiniteFromString, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Result.succeed("1"), Result.succeed(1)) await decoding.succeed(Result.fail("2"), Result.fail(2)) await decoding.fail(null, `Expected Result, got null`) await decoding.fail( Result.succeed("a"), `Expected a finite number, got NaN at ["success"]` ) await decoding.fail( Result.fail("b"), `Expected a finite number, got NaN at ["failure"]` ) const encoding = asserts.encoding() await encoding.succeed(Result.succeed(1), Result.succeed("1")) await encoding.succeed(Result.fail(2), Result.fail("2")) }) }) it("Defect", async () => { const schema = Schema.Defect() const asserts = new TestSchema.Asserts(schema) const noPrototypeObject = Object.create(null) noPrototypeObject.message = "a" if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() // Error: message only await decoding.succeed({ message: "a" }, new Error("a")) await decoding.succeed(noPrototypeObject, new Error("a")) // Error: message and name await decoding.succeed( { message: "a", name: "b" }, (() => { const err = new Error("a") err.name = "b" return err })() ) // Error: message, name, and stack await decoding.succeed( { message: "a", name: "b", stack: "c" }, (() => { const err = new Error("a") err.name = "b" err.stack = "c" return err })() ) // anything else await decoding.succeed("a") await decoding.succeed({ a: 1 }) const encoding = asserts.encoding() // Error await encoding.succeed(new Error("a"), { name: "Error", message: "a" }) // anything else await encoding.succeed("a") await encoding.succeed({ a: 1 }) await encoding.succeed(noPrototypeObject, { message: "a" }) }) it("Error and Defect memoize equivalent options", () => { const assertMemoized = (schema: (options?: Schema.ErrorOptions) => S) => { strictEqual(schema(), schema({})) strictEqual(schema(), schema({ includeStack: false })) strictEqual(schema(), schema({ excludeCause: false })) strictEqual(schema(), schema({ includeStack: false, excludeCause: false })) strictEqual(schema({ includeStack: true }), schema({ includeStack: true })) strictEqual(schema({ includeStack: true }), schema({ includeStack: true, excludeCause: false })) strictEqual(schema({ excludeCause: true }), schema({ excludeCause: true })) strictEqual(schema({ excludeCause: true }), schema({ includeStack: false, excludeCause: true })) strictEqual( schema({ includeStack: true, excludeCause: true }), schema({ includeStack: true, excludeCause: true }) ) assertFalse(schema() === schema({ includeStack: true })) assertFalse(schema() === schema({ excludeCause: true })) assertFalse(schema({ includeStack: true }) === schema({ includeStack: true, excludeCause: true })) assertFalse(schema({ excludeCause: true }) === schema({ includeStack: true, excludeCause: true })) } assertMemoized(Schema.Error) assertMemoized(Schema.Defect) }) describe("CauseReason", () => { it("should expose the values", () => { const schema = Schema.CauseReason(Schema.String, Schema.Number) strictEqual(schema.error, Schema.String) strictEqual(schema.annotate({}).error, Schema.String) strictEqual(schema.defect, Schema.Number) strictEqual(schema.annotate({}).defect, Schema.Number) }) it("CauseReason(FiniteFromString, FiniteFromString)", async () => { const schema = Schema.CauseReason(Schema.FiniteFromString, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) }) describe("Cause", () => { it("should expose the values", () => { const schema = Schema.Cause(Schema.String, Schema.Number) strictEqual(schema.error, Schema.String) strictEqual(schema.annotate({}).error, Schema.String) strictEqual(schema.defect, Schema.Number) strictEqual(schema.annotate({}).defect, Schema.Number) }) it("Cause(FiniteFromString, FiniteFromString)", async () => { const schema = Schema.Cause(Schema.FiniteFromString, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Cause.fail("1"), Cause.fail(1)) await decoding.succeed(Cause.die("2"), Cause.die(2)) await decoding.succeed(Cause.interrupt(3)) await decoding.fail( Cause.fail("a"), `Expected a finite number, got NaN at ["failures"][0]["error"]` ) await decoding.fail( Cause.die("a"), `Expected a finite number, got NaN at ["failures"][0]["defect"]` ) const encoding = asserts.encoding() await encoding.succeed(Cause.fail(1), Cause.fail("1")) await encoding.succeed(Cause.die(2), Cause.die("2")) await encoding.succeed(Cause.interrupt(3)) await encoding.fail( Cause.fail("a"), `Expected number, got "a" at ["failures"][0]["error"]` ) await encoding.fail( Cause.die("a"), `Expected number, got "a" at ["failures"][0]["defect"]` ) }) }) it("Error", async () => { const schema = Schema.Error() const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const error = new Error("a") const customError = new Error("b") customError.name = "CustomError" customError.stack = "stack" const decoding = asserts.decoding() await decoding.succeed(error) await decoding.succeed(customError) await decoding.fail( { message: "a" }, `Expected Error, got {"message":"a"}` ) await decoding.fail( "a", `Expected Error, got "a"` ) const encoding = asserts.encoding() await encoding.succeed(error) await encoding.succeed(customError) await encoding.fail( { message: "a" }, `Expected Error, got {"message":"a"}` ) await encoding.fail( "a", `Expected Error, got "a"` ) }) describe("Exit", () => { it("should expose the values", () => { const schema = Schema.Exit(Schema.String, Schema.Number, Schema.Boolean) strictEqual(schema.value, Schema.String) strictEqual(schema.annotate({}).value, Schema.String) strictEqual(schema.error, Schema.Number) strictEqual(schema.annotate({}).error, Schema.Number) strictEqual(schema.defect, Schema.Boolean) strictEqual(schema.annotate({}).defect, Schema.Boolean) }) it("Exit(FiniteFromString, String, Unknown)", async () => { const schema = Schema.Exit(Schema.FiniteFromString, Schema.String, Schema.Unknown) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Exit.succeed("123"), Exit.succeed(123)) await decoding.succeed(Exit.fail("boom")) await decoding.fail( null, `Expected Exit, got null` ) await decoding.fail( Exit.succeed(123), `Expected string, got 123 at ["value"]` ) await decoding.fail( Exit.fail(null), `Expected string, got null at ["cause"]["failures"][0]["error"]` ) }) it("Exit(FiniteFromString, String, Defect)", async () => { const schema = Schema.Exit(Schema.FiniteFromString, Schema.String, Schema.Defect()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() const boomError = new Error("boom message") boomError.name = "boom" await decoding.succeed( Exit.die({ name: "boom", message: "boom message" }), Exit.die(boomError) ) }) }) describe("suspend", () => { it("should work", async () => { interface Category { readonly a: A readonly categories: ReadonlyArray } interface CategoryType extends Category {} interface CategoryEncoded extends Category {} const schema = Schema.Struct({ a: Schema.FiniteFromString.check(Schema.isGreaterThan(0)), categories: Schema.Array(Schema.suspend((): Schema.Codec => schema)) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1", categories: [] }, { a: 1, categories: [] }) await decoding.succeed({ a: "1", categories: [{ a: "2", categories: [] }] }, { a: 1, categories: [{ a: 2, categories: [] }] }) await decoding.fail( { a: "1", categories: [{ a: "a", categories: [] }] }, `Expected a finite number, got NaN at ["categories"][0]["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: 1, categories: [] }, { a: "1", categories: [] }) await encoding.succeed({ a: 1, categories: [{ a: 2, categories: [] }] }, { a: "1", categories: [{ a: "2", categories: [] }] }) await encoding.fail( { a: 1, categories: [{ a: -1, categories: [] }] }, `Expected a value greater than 0, got -1 at ["categories"][0]["a"]` ) }) }) describe("make", () => { it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( Cause.fail(new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(cause))) }) throws(() => schema.make({}), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Constructor adapter can only throw schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) }) describe("makeOption", () => { it("Struct", () => { const schema = Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) }) deepStrictEqual(schema.makeOption({ a: 1 }), Option.some({ a: 1 })) deepStrictEqual(schema.makeOption({ a: -1 }), Option.none()) }) it("Class", () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })) {} deepStrictEqual(A.makeOption({ a: 1 }), Option.some(new A({ a: 1 }))) deepStrictEqual(A.makeOption({ a: -1 }), Option.none()) }) it("should throw an error when the cause is not a schema issue", () => { const schema = Schema.Struct({ a: Schema.String.pipe(Schema.withConstructorDefault(Effect.die(new Error("make defect")))) }) class A extends Schema.Class("A")(schema) {} throws(() => schema.makeOption({}), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) throws(() => A.makeOption(), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) }) describe("makeEffect", () => { it.effect("Struct", () => Effect.gen(function*() { const schema = Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) }) const success = yield* schema.makeEffect({ a: 1 }).pipe(Effect.result) deepStrictEqual(success, Result.succeed({ a: 1 })) const failure = yield* schema.makeEffect({ a: -1 }).pipe(Effect.flip) assertTrue(Schema.isSchemaError(failure)) })) it.effect("Class", () => Effect.gen(function*() { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.Number.check(Schema.isGreaterThan(0)) })) {} const success = yield* A.makeEffect({ a: 1 }) deepStrictEqual(success, new A({ a: 1 })) const failure = yield* A.makeEffect({ a: -1 }).pipe(Effect.flip) assertTrue(Schema.isSchemaError(failure)) })) it.effect("should preserve mixed schema error and defect causes", () => Effect.gen(function*() { const cause = Cause.combine( Cause.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })) ), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(cause))) }) const exit = yield* schema.makeEffect({}).pipe(Effect.exit) assertTrue(Exit.isFailure(exit)) assertTrue(Exit.hasDies(exit)) const error = Cause.findError(exit.cause) assertTrue(Result.isSuccess(error)) assertTrue(Schema.isSchemaError(error.success)) })) }) describe("withConstructorDefault", () => { describe("Struct", () => { it("should not apply defaults when decoding / encoding", async () => { const schema = Schema.Struct({ a: Schema.String.pipe(Schema.optionalKey, Schema.withConstructorDefault(Effect.succeed("a"))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, {}) const encoding = asserts.encoding() await encoding.succeed({}, {}) }) it("should apply constructor default when the field is not present", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({}, { a: -1 }) }) describe("nested defaults", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }).pipe(Schema.withConstructorDefault(Effect.succeed({}))) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: { b: 1 } }) await make.succeed({ a: {} }, { a: { b: -1 } }) await make.succeed({}, { a: { b: -1 } }) }) it("Class", async () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }).pipe(Schema.withConstructorDefault(Effect.succeed({}))) })) {} const asserts = new TestSchema.Asserts(A) const make = asserts.make() await make.succeed({ a: { b: 1 } }, new A({ a: { b: 1 } })) await make.succeed({ a: {} }, new A({ a: { b: -1 } })) await make.succeed({}, new A({ a: { b: -1 } })) deepStrictEqual(A.make({ a: { b: 1 } }), new A({ a: { b: 1 } })) deepStrictEqual(A.make({ a: {} }), new A({ a: { b: -1 } })) deepStrictEqual(A.make({}), new A({ a: { b: -1 } })) }) }) it("applies constructor default when disableChecks is true", () => { class A extends Schema.Class("A")({ a: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("default"))) }) {} const instance = new A({}, { disableChecks: true }) deepStrictEqual(instance.a, "default") }) it("Struct & Effect sync", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({}, { a: -1 }) }) it("Struct & Effect async", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault( Effect.gen(function*() { yield* Effect.sleep(100) return -1 }) )) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({}, { a: -1 }) }) it("Effect failing with SchemaError propagates as parse failure", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault( Effect.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "ctor default failed" })) ) )) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.fail( {}, `ctor default failed at ["a"]` ) }) it("Effect failing via another schema's makeEffect propagates as parse failure", async () => { const Inner = Schema.Struct({ n: Schema.FiniteFromString.pipe(Schema.check(Schema.isGreaterThan(0))) }) const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault( Inner.makeEffect({ n: -1 }).pipe(Effect.map((s) => s.n)) )) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.fail( {}, `Expected a value greater than 0, got -1 at ["a"]["n"]` ) }) it("Struct & Effect async & service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withConstructorDefault( Effect.gen(function*() { yield* Effect.sleep(100) const oservice = yield* Effect.serviceOption(Service) if (Option.isNone(oservice)) { return -1 } return yield* oservice.value.value }) )) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({}, { a: -1 }) const effect = await schema.makeEffect({}).pipe( Effect.provideService(Service, { value: Effect.succeed(0) }), Effect.result, Effect.runPromise ) deepStrictEqual(effect, Result.succeed({ a: 0 })) }) }) describe("Tuple", () => { it("Tuple & Some", async () => { const schema = Schema.Tuple( [Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1)))] ) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed([1]) await make.succeed([], [-1]) }) it("nested defaults (Struct)", async () => { const schema = Schema.Tuple( [ Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) }).pipe(Schema.withConstructorDefault(Effect.succeed({}))) ] ) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed([{ b: 1 }]) await make.succeed([{}], [{ b: -1 }]) await make.succeed([], [{ b: -1 }]) }) it("inner defaults (Tuple)", async () => { const schema = Schema.Tuple( [ Schema.Tuple([ Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) ]) ] ) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed([[1]]) await make.succeed([[]], [[-1]]) }) it("nested defaults (Tuple)", async () => { const schema = Schema.Tuple( [ Schema.Tuple([ Schema.FiniteFromString.pipe(Schema.withConstructorDefault(Effect.succeed(-1))) ]).pipe(Schema.withConstructorDefault(Effect.succeed([] as const))) ] ) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed([[1]]) await make.succeed([[]], [[-1]]) await make.succeed([], [[-1]]) }) }) }) describe("Record", () => { it("Record(String, Number)", async () => { const schema = Schema.Record(Schema.String, Schema.Number) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.fail(null, `Expected object, got null`) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.fail(null, "Expected object, got null") await decoding.fail( { a: "b" }, `Expected number, got "b" at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }) await encoding.fail( { a: "b" }, `Expected number, got "b" at ["a"]` ) await encoding.fail(null, "Expected object, got null") }) it("Record(String, optionalKey(Number)) should throw", async () => { throws( () => Schema.Record(Schema.String, Schema.optionalKey(Schema.Number)), new Error("Cannot use `Schema.optionalKey` with index signatures, use `Schema.optional` instead.") ) }) it("Record(String, optional(Number))", async () => { const schema = Schema.Record(Schema.String, Schema.optional(Schema.Number)) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: 1 }) await make.succeed({ a: undefined }) await make.fail(null, `Expected object, got null`) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.succeed({ a: undefined }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }) await encoding.succeed({ a: undefined }) }) it("Record(String.check, Number) should use the key checks to select keys", async () => { const schema = Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.Number) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, ab: 2, b: "ignored" }, { a: 1, ab: 2 }) await decoding.fail( { a: "bad", b: 1 }, `Expected number, got "bad" at ["a"]` ) }) it("Record(Symbol, Number)", async () => { const schema = Schema.Record(Schema.Symbol, Schema.Number) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ [Symbol.for("a")]: 1 }) await make.fail(null, `Expected object, got null`) const decoding = asserts.decoding() await decoding.succeed({ [Symbol.for("a")]: 1 }) await decoding.fail(null, "Expected object, got null") await decoding.fail( { [Symbol.for("a")]: "b" }, `Expected number, got "b" at [Symbol(a)]` ) const encoding = asserts.encoding() await encoding.succeed({ [Symbol.for("a")]: 1 }) await encoding.fail( { [Symbol.for("a")]: "b" }, `Expected number, got "b" at [Symbol(a)]` ) await encoding.fail(null, "Expected object, got null") }) it("Record(Symbol.check, Number) should use the key checks to select keys", async () => { const a = Symbol.for("a") const b = Symbol.for("b") const schema = Schema.Record( Schema.Symbol.check(Schema.makeFilter((symbol) => Symbol.keyFor(symbol)?.startsWith("a") ?? false)), Schema.Number ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ [a]: 1, [b]: "ignored" }, { [a]: 1 }) await decoding.fail( { [a]: "bad", [b]: 1 }, `Expected number, got "bad" at [Symbol(a)]` ) }) it("Record(SnakeToCamel, NumberFromString)", async () => { const schema = Schema.Record(SnakeToCamel, Schema.NumberFromString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({ a_b: "1" }, { aB: 1 }) await decoding.succeed({ a_b: "1", aB: "2" }, { aB: 2 }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({ aB: 1 }, { a_b: "1" }) await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "2" }) }) it("Record(SnakeToCamel, Number, { keyValueCombiner: ... })", async () => { const schema = Schema.Record(SnakeToCamel, Schema.NumberFromString, { keyValueCombiner: { decode: { combine: ([_, v1], [k2, v2]) => [k2, v1 + v2] }, encode: { combine: ([_, v1], [k2, v2]) => [k2, v1 + "e" + v2] } } }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({ a_b: "1" }, { aB: 1 }) await decoding.succeed({ a_b: "1", aB: "2" }, { aB: 3 }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({ aB: 1 }, { a_b: "1" }) await encoding.succeed({ a_b: 1, aB: 2 }, { a_b: "1e2" }) }) it("UniqueSymbol", async () => { const a = Symbol.for("a") const schema = Schema.Record(Schema.UniqueSymbol(a), Schema.Number) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ [a]: 1 }) await decoding.fail( { [a]: "b" }, `Expected number, got "b" at [Symbol(a)]` ) }) describe("Literals keys", () => { it("Record(Literals, Number)", async () => { const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.Number) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 1 }, `Missing key at ["b"]` ) await decoding.fail( { b: 2 }, `Missing key at ["a"]` ) }) it("Record(Literals, optionalKey(Number))", async () => { const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.optionalKey(Schema.Number)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed({ a: 1 }) await decoding.succeed({ b: 2 }) await decoding.succeed({ a: 1, b: 2 }) }) it("Record(Literals, mutableKey(Number))", async () => { const schema = Schema.Record(Schema.Literals(["a", "b"]), Schema.mutableKey(Schema.Number)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1, b: 2 }) }) it("Record(Literals, mutableKey(optionalKey(Number)))", async () => { const schema = Schema.Record( Schema.Literals(["a", "b"]), Schema.mutableKey(Schema.optionalKey(Schema.Number)) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}) await decoding.succeed({ a: 1 }) await decoding.succeed({ b: 2 }) await decoding.succeed({ a: 1, b: 2 }) }) }) it("Record(Number, String)", async () => { const schema = Schema.Record(Schema.Number, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ 1: "1", 2.2: "2", Infinity: "3", NaN: "4", "-Infinity": "5" }, { "1": 1, "2.2": 2, Infinity: 3, NaN: 4, "-Infinity": 5 }) await decoding.fail( { 1: null }, `Expected string, got null at ["1"]` ) await decoding.fail( { 1: "a" }, `Expected a finite number, got NaN at ["1"]` ) }) it("Record(Int, String)", async () => { const schema = Schema.Record(Schema.Int, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ 1: "1" }, { "1": 1 }) await decoding.succeed({ 1: "1", "1.1": "ignored", Infinity: "ignored", NaN: "ignored" }, { "1": 1 }) await decoding.fail( { 1: null }, `Expected string, got null at ["1"]` ) await decoding.fail( { 1: "a" }, `Expected a finite number, got NaN at ["1"]` ) }) it("Record(TemplateLiteral with checked part, Number) should use the part checks to select keys", async () => { const schema = Schema.Record(Schema.TemplateLiteral(["a", Schema.NonEmptyString]), Schema.Number) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "ignored", ab: 1 }, { ab: 1 }) await decoding.fail( { a: 1, ab: "bad" }, `Expected number, got "bad" at ["ab"]` ) }) it(`Record(Union(Number, "a"), FiniteFromString)`, async () => { const schema = Schema.Record(Schema.Union([Schema.Number, Schema.Literal("a")]), Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed( { a: "-1", 1: "1", 2.2: "2", Infinity: "3", NaN: "4", "-Infinity": "5" }, { a: -1, "1": 1, "2.2": 2, Infinity: 3, NaN: 4, "-Infinity": 5 } ) }) }) describe("Union", () => { it("empty", async () => { const schema = Schema.Union([]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail(null, `Expected never, got null`) }) it(`String`, async () => { const schema = Schema.Union([Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(null, `Expected string, got null`) }) it(`Void`, async () => { const schema = Schema.Union([Schema.Void]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(undefined) await decoding.succeed(null, undefined) await decoding.succeed("a", undefined) await decoding.succeed(1, undefined) await decoding.succeed({}, undefined) await decoding.succeed([], undefined) }) it(`Void | String`, async () => { const schema = Schema.Union([Schema.Void, Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a", undefined) }) it(`String | Number`, async () => { const schema = Schema.Union([Schema.String, Schema.Number]) const asserts = new TestSchema.Asserts(schema) deepStrictEqual(schema.members, [Schema.String, Schema.Number]) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(1) await decoding.fail( null, `Expected string | number, got null` ) }) it(`String | Never`, async () => { const schema = Schema.Union([Schema.String, Schema.Never]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(null, `Expected string | never, got null`) }) it(`String & isMinLength(1) | number & isGreaterThan(0)`, async () => { const schema = Schema.Union([ Schema.NonEmptyString, Schema.Number.check(Schema.isGreaterThan(0)) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(1) await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) await decoding.fail( -1, `Expected a value greater than 0, got -1` ) }) it(`mode: "oneOf"`, async () => { const schema = Schema.Union([ Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number }) ], { mode: "oneOf" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }) await decoding.succeed({ b: 1 }) await decoding.fail( { a: "a", b: 1 }, `Expected exactly one member to match the input {"a":"a","b":1}` ) }) it(`mode: "oneOf" with different sentinel keys`, async () => { const schema = Schema.Union([ Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }), Schema.Struct({ status: Schema.Literal("ready"), value: Schema.String }) ], { mode: "oneOf" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( { kind: "a", status: "ready", value: "value" }, `Expected exactly one member to match the input {"kind":"a","status":"ready","value":"value"}` ) }) it(`mode: "oneOf" counts repeated member occurrences`, async () => { const member = Schema.Struct({ kind: Schema.Literal("a") }) const schema = Schema.Union([member, member], { mode: "oneOf" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( { kind: "a" }, `Expected exactly one member to match the input {"kind":"a"}` ) }) it("preserves member order after sentinel dispatch", async () => { const fallback = Schema.Struct({ value: Schema.String }).pipe( Schema.decodeTo(Schema.String, { decode: SchemaGetter.transform(() => "fallback"), encode: SchemaGetter.transform((value) => ({ value })) }) ) const discriminated = Schema.Struct({ kind: Schema.Literal("a"), value: Schema.String }).pipe( Schema.decodeTo(Schema.String, { decode: SchemaGetter.transform(() => "discriminated"), encode: SchemaGetter.transform((value) => ({ kind: "a" as const, value })) }) ) const schema = Schema.Union([fallback, discriminated]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ kind: "a", value: "value" }, "fallback") }) it.effect("preserves member order with concurrent decoding", () => Effect.gen(function*() { const firstLatch = yield* Deferred.make() const secondCompleted = yield* Deferred.make() const first = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.await(firstLatch).pipe(Effect.as("first"))), encode: SchemaGetter.passthrough() }) ) const second = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second")) ), encode: SchemaGetter.passthrough() }) ) const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", { concurrency: 2 }).pipe(Effect.forkChild) yield* Deferred.await(secondCompleted) yield* Effect.yieldNow yield* Deferred.succeed(firstLatch, undefined) strictEqual(yield* Fiber.join(fiber), "first") })) it.effect("uses a buffered concurrent success after earlier candidates fail", () => Effect.gen(function*() { const firstLatch = yield* Deferred.make() const secondCompleted = yield* Deferred.make() const first = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail((value) => Deferred.await(firstLatch).pipe( Effect.andThen(Effect.fail(new SchemaIssue.Forbidden(Option.some(value), { message: "first failed" }))) ) ), encode: SchemaGetter.passthrough() }) ) const second = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second")) ), encode: SchemaGetter.passthrough() }) ) const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", { concurrency: 2 }).pipe(Effect.forkChild) yield* Deferred.await(secondCompleted) yield* Effect.yieldNow yield* Deferred.succeed(firstLatch, undefined) strictEqual(yield* Fiber.join(fiber), "second") })) it.effect(`mode: "oneOf" detects concurrent successes in member order`, () => Effect.gen(function*() { const firstLatch = yield* Deferred.make() const secondCompleted = yield* Deferred.make() const first = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.await(firstLatch).pipe(Effect.as("first"))), encode: SchemaGetter.passthrough() }) ) const second = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.succeed(secondCompleted, undefined).pipe(Effect.as("second")) ), encode: SchemaGetter.passthrough() }) ) const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second], { mode: "oneOf" }))( "value", { concurrency: 2 } ).pipe(Effect.exit, Effect.forkChild) yield* Deferred.await(secondCompleted) yield* Effect.yieldNow yield* Deferred.succeed(firstLatch, undefined) const exit = yield* Fiber.join(fiber) strictEqual(exit._tag, "Failure") if (exit._tag === "Failure") { const reason = exit.cause.reasons[0] strictEqual(reason._tag, "Fail") if (reason._tag === "Fail") { assertTrue(Schema.isSchemaError(reason.error)) if (Schema.isSchemaError(reason.error)) { strictEqual(reason.error.issue._tag, "OneOf") if (reason.error.issue._tag === "OneOf") { deepStrictEqual(reason.error.issue.successes, [first.ast, second.ast]) } } } } })) it.effect("interrupts pending concurrent members after anyOf succeeds", () => Effect.gen(function*() { const firstStarted = yield* Deferred.make() const firstLatch = yield* Deferred.make() const secondStarted = yield* Deferred.make() const secondInterrupted = yield* Deferred.make() const first = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.succeed(firstStarted, undefined).pipe( Effect.andThen(Deferred.await(firstLatch)), Effect.as("first") ) ), encode: SchemaGetter.passthrough() }) ) const second = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.transformOrFail(() => Deferred.succeed(secondStarted, undefined).pipe( Effect.andThen(Effect.never), Effect.onInterrupt(() => Deferred.succeed(secondInterrupted, undefined).pipe(Effect.asVoid)) ) ), encode: SchemaGetter.passthrough() }) ) const fiber = yield* Schema.decodeUnknownEffect(Schema.Union([first, second]))("value", { concurrency: 2 }).pipe(Effect.forkChild) yield* Deferred.await(firstStarted) yield* Deferred.await(secondStarted) yield* Deferred.succeed(firstLatch, undefined) strictEqual(yield* Fiber.join(fiber), "first") yield* Deferred.await(secondInterrupted) })) it(`mode: "oneOf" with Void`, async () => { const schema = Schema.Union([Schema.Void, Schema.String], { mode: "oneOf" }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail("a", `Expected exactly one member to match the input "a"`) }) it("{} & Literal", async () => { const schema = Schema.Union([ Schema.Struct({}), Schema.Literal("a") ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed([]) }) describe("should exclude members based on failed sentinels", () => { it("string | struct", async () => { const schema = Schema.Union([ Schema.String, Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String }) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( {}, `Expected string | { readonly "_tag": "a", ... }, got {}` ) }) it("string | tuple", async () => { const schema = Schema.Union([ Schema.String, Schema.Tuple([Schema.Literal("a"), Schema.String]) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( [], `Expected string | readonly [ "a", ... ], got []` ) }) it("tagged struct union", async () => { const schema = Schema.Union([ Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String }), Schema.Struct({ _tag: Schema.Literal("b"), b: Schema.Number }) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( { _tag: "a" }, `Missing key at ["a"]` ) await decoding.fail( { _tag: "b" }, `Missing key at ["b"]` ) await decoding.fail( { _tag: "c" }, `Expected { readonly "_tag": "a", ... } | { readonly "_tag": "b", ... }, got {"_tag":"c"}` ) }) it("tagged tuple union", async () => { const schema = Schema.Union([ Schema.Tuple([Schema.Literal("a"), Schema.String]), Schema.Tuple([Schema.Literal("b"), Schema.Number]) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( ["a"], `Missing key at [1]` ) await decoding.fail( ["b"], `Missing key at [1]` ) await decoding.fail( ["c"], `Expected readonly [ "a", ... ] | readonly [ "b", ... ], got ["c"]` ) }) }) }) describe("TupleWithRest", () => { it("A required element cannot follow an optional element", () => { throws( () => Schema.TupleWithRest( Schema.Tuple([Schema.optionalKey(Schema.String)]), [Schema.Boolean, Schema.String] ), new Error("A required element cannot follow an optional element. ts(1257)") ) throws( () => Schema.TupleWithRest( Schema.Tuple([Schema.optional(Schema.String)]), [Schema.Boolean, Schema.String] ), new Error("A required element cannot follow an optional element. ts(1257)") ) }) it("An optional element cannot follow a rest element", () => { throws( () => Schema.TupleWithRest( Schema.Tuple([]), [Schema.Boolean, Schema.optionalKey(Schema.String)] ), new Error("An optional element cannot follow a rest element. ts(1266)") ) throws( () => Schema.TupleWithRest( Schema.Tuple([]), [Schema.Boolean, Schema.optional(Schema.String)] ), new Error("An optional element cannot follow a rest element. ts(1266)") ) }) it("[FiniteFromString, String] + [Boolean, String]", async () => { const schema = Schema.TupleWithRest( Schema.Tuple([Schema.FiniteFromString, Schema.String]), [Schema.Boolean, Schema.String] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(["1", "a", "b"], [1, "a", "b"]) await decoding.succeed(["1", "a", true, "b"], [1, "a", true, "b"]) await decoding.succeed(["1", "a", true, true, "b"], [1, "a", true, true, "b"]) await decoding.fail( ["1", "a"], `Missing key at [2]` ) await decoding.fail( ["1", "a", true], `Expected string, got true at [2]` ) await decoding.fail( ["1", "a", "b", "c"], `Expected boolean, got "b" at [2]` ) await decoding.fail( ["1", "a", true, "b", "c"], `Expected boolean, got "b" at [3]` ) const encoding = asserts.encoding() await encoding.succeed([1, "a", "b"], ["1", "a", "b"]) await encoding.succeed([1, "a", true, "b"], ["1", "a", true, "b"]) }) it("[FiniteFromString, String] + [Boolean, String, Number]", async () => { const schema = Schema.TupleWithRest( Schema.Tuple([Schema.FiniteFromString, Schema.String]), [Schema.Boolean, Schema.String, Schema.FiniteFromString] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(["1", "a", "b", "2"], [1, "a", "b", 2]) await decoding.succeed(["1", "a", true, "b", "2"], [1, "a", true, "b", 2]) await decoding.succeed(["1", "a", true, true, "b", "2"], [1, "a", true, true, "b", 2]) await decoding.fail( ["1", "a"], `Missing key at [2]` ) await decoding.fail( ["1", "a", "b"], `Missing key at [3]` ) await decoding.fail( ["1", "a", "b", "c"], `Expected a finite number, got NaN at [3]` ) const encoding = asserts.encoding() await encoding.succeed([1, "a", "b", 2], ["1", "a", "b", "2"]) await encoding.succeed([1, "a", true, "b", 2], ["1", "a", true, "b", "2"]) }) it("[String] + [Boolean, String, Number, Number] validates every post-rest index", async () => { const schema = Schema.TupleWithRest( Schema.Tuple([Schema.String]), [Schema.Boolean, Schema.String, Schema.FiniteFromString, Schema.FiniteFromString] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( ["a", true, "b", "1", "x"], `Expected a finite number, got NaN at [4]` ) await decoding.succeed(["a", true, "b", "1", "2"], ["a", true, "b", 1, 2]) }) }) describe("StructWithRest", () => { it("should throw an error if there are encodings", () => { throws( () => Schema.StructWithRest( Schema.Struct({}).pipe(Schema.encodeTo(Schema.Struct({}))), [Schema.Record(Schema.String, Schema.Number)] ), new Error(`StructWithRest does not support encodings`) ) }) it("Record(String, Number)", async () => { const schema = Schema.StructWithRest( Schema.Struct({ a: Schema.Number }), [Schema.Record(Schema.String, Schema.Number)] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 1, b: "" }, `Expected number, got "" at ["b"]` ) }) it("Record(Symbol, Number)", async () => { const schema = Schema.StructWithRest( Schema.Struct({ a: Schema.Number }), [Schema.Record(Schema.Symbol, Schema.Number)] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.succeed({ a: 1, [Symbol.for("b")]: 2 }) await decoding.fail( { a: 1, [Symbol.for("b")]: "c" }, `Expected number, got "c" at [Symbol(b)]` ) }) it("Record(`a${string}`, Number)", async () => { const schema = Schema.StructWithRest( Schema.Struct({ a: Schema.Number }), [Schema.Record(Schema.TemplateLiteral(["a", Schema.String]), Schema.Finite)] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.succeed({ a: 1, "ab": 2 }) await decoding.fail( { a: NaN, "ab": 2 }, `Expected a finite number, got NaN at ["a"]` ) await decoding.fail( { a: 1, "ab": "c" }, `Expected number, got "c" at ["ab"]` ) }) it("should preserve both checks", async () => { const schema = Schema.StructWithRest( Schema.Struct({ a: Schema.Number }).check( Schema.makeFilter((s) => s.a > 0, { expected: "agt(0)" }) ), [ Schema.Record(Schema.String, Schema.Number).check( Schema.makeFilter((s) => s.b === undefined || s.b > 1, { expected: "bgt(1)" }) ) ] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: 1 }) await decoding.succeed({ a: 1, b: 2 }) await decoding.fail( { a: 0 }, `Expected agt(0), got {"a":0}` ) await decoding.fail( { a: 1, b: 1 }, `Expected bgt(1), got {"a":1,"b":1}` ) }) it("index signatures should not re-parse fixed properties", async () => { const Trimmed = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.trim())) const schema = Schema.StructWithRest( Schema.Struct({ a: Trimmed }), [Schema.Record(Schema.String, Schema.String)] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: " x ", b: "y" }, { a: "x", b: "y" }) const encoding = asserts.encoding() await encoding.succeed({ a: "x", b: "y" }) }) it("index signatures should not overwrite fixed properties after key decoding", async () => { const UppercaseKey = Schema.String.pipe(Schema.decodeTo(Schema.String, SchemaTransformation.toUpperCase())) const schema = Schema.StructWithRest( Schema.Struct({ A: Schema.String }), [Schema.Record(UppercaseKey, Schema.String)] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ A: "fixed", a: "rest" }, { A: "fixed" }) }) }) describe("NullOr", () => { it("NullOr(String)", async () => { const schema = Schema.NullOr(Schema.NonEmptyString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(null) await decoding.fail(undefined, `Expected string | null, got undefined`) await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) }) }) describe("UndefinedOr", () => { it("UndefinedOr(String)", async () => { const schema = Schema.UndefinedOr(Schema.NonEmptyString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(undefined) await decoding.fail(null, `Expected string | undefined, got null`) await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) }) }) describe("NullishOr", () => { it("NullishOr(String)", async () => { const schema = Schema.NullishOr(Schema.NonEmptyString) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(null) await decoding.succeed(undefined) await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) }) }) it("PropertyKey", async () => { const schema = Schema.PropertyKey const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("BooleanFromBit", async () => { const schema = Schema.BooleanFromBit const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("Uint8Array", async () => { const schema = Schema.Uint8Array const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("Uint8ArrayFromBase64", async () => { const schema = Schema.Uint8ArrayFromBase64 const asserts = new TestSchema.Asserts(schema) const encoder = new TextEncoder() if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", encoder.encode("foobar")) await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5") await decoding.fail("Zm9vYmF-", "Invalid character -") await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end") const encoding = asserts.encoding() await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy") }) it("StringFromBase64", async () => { const schema = Schema.StringFromBase64 const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", "foobar") await decoding.fail("Zm9vY", "Length must be a multiple of 4, but is 5") await decoding.fail("Zm9vYmF-", "Invalid character -") await decoding.fail("=Zm9vYmF", "Found a '=' character, but it is not at the end") const encoding = asserts.encoding() await encoding.succeed("foobar", "Zm9vYmFy") }) it("StringFromBase64Url", async () => { const schema = Schema.StringFromBase64Url const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", "foobar") await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5") await decoding.succeed("Pj8-ZD_Dnw", ">?>d?\u00DF") await decoding.fail("Pj8/ZD+Dnw", "Invalid input") const encoding = asserts.encoding() await encoding.succeed("foobar", "Zm9vYmFy") await encoding.succeed(">?>d?\u00DF", "Pj8-ZD_Dnw") }) it("StringFromHex", async () => { const schema = Schema.StringFromHex const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("67", "g") await decoding.fail("0", "Length must be a multiple of 2, but is 1") await decoding.fail("zd4aa", "Length must be a multiple of 2, but is 5") await decoding.fail("0\x01", "Invalid input") const encoding = asserts.encoding() await encoding.succeed("g", "67") }) it("StringFromUriComponent", async () => { const schema = Schema.StringFromUriComponent const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("%7B%22a%22%3A1%7D", "{\"a\":1}") await decoding.succeed("%D1%88%D0%B5%D0%BB%D0%BB%D1%8B", "шеллы") await decoding.succeed("hello%20world", "hello world") await decoding.succeed("hello", "hello") await decoding.fail("%ZZ", `URI malformed`) const encoding = asserts.encoding() await encoding.succeed("{\"a\":1}", "%7B%22a%22%3A1%7D") await encoding.succeed("hello world", "hello%20world") }) it("Uint8ArrayFromBase64Url", async () => { const schema = Schema.Uint8ArrayFromBase64Url const asserts = new TestSchema.Asserts(schema) const encoder = new TextEncoder() if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("Zm9vYmFy", encoder.encode("foobar")) await decoding.fail("Zm9vY", "Length should be a multiple of 4, but is 5") await decoding.succeed("Pj8-ZD_Dnw", encoder.encode(">?>d?ß")) await decoding.fail("Pj8/ZD+Dnw", "Invalid input") const encoding = asserts.encoding() await encoding.succeed(encoder.encode("foobar"), "Zm9vYmFy") await encoding.succeed(encoder.encode(">?>d?ß"), "Pj8-ZD_Dnw") }) it("Uint8ArrayFromHex", async () => { const schema = Schema.Uint8ArrayFromHex const asserts = new TestSchema.Asserts(schema) const encoder = new TextEncoder() if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed( "0001020304050607", Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]) ) await decoding.succeed( "f0f1f2f3f4f5f6f7", Uint8Array.from([0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]) ) await decoding.succeed("67", encoder.encode("g")) await decoding.fail("0", "Length must be a multiple of 2, but is 1") await decoding.fail("2d4aa", "Length must be a multiple of 2, but is 5") await decoding.fail("0\x01", "Invalid input") const encoding = asserts.encoding() await encoding.succeed(Uint8Array.from([0, 1, 2, 3, 4, 5, 6, 7]), "0001020304050607") }) it("Date", async () => { const schema = Schema.Date const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(new Date("2021-01-01")) await decoding.fail(null, `Expected Date, got null`) await decoding.fail(0, `Expected Date, got 0`) }) it("DateValid", async () => { const schema = Schema.DateValid const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("DateTimeUtc", async () => { const schema = Schema.DateTimeUtc const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) }) it("DateTimeUtcFromValidDate", async () => { const schema = Schema.DateTimeUtcFromDate const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(new Date("2021-01-01T00:00:00.000Z"), DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) await decoding.fail(new Date("invalid date"), `Expected a valid date, got Invalid Date`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), new Date("2021-01-01T00:00:00.000Z")) }) it("DateTimeUtcFromString", async () => { const schema = Schema.DateTimeUtcFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("2021-01-01T00:00:00.000Z", DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) await decoding.fail("invalid", `Invalid UTC DateTime string: invalid`) await decoding.fail(null, `Expected string, got null`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), "2021-01-01T00:00:00.000Z") }) it("DateTimeUtcFromMillis", async () => { const schema = Schema.DateTimeUtcFromMillis const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(1609459200000, DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")) await decoding.fail(null, `Expected number, got null`) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), 1609459200000) }) it("TimeZoneOffset", async () => { const schema = Schema.TimeZoneOffset const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) const encoding = asserts.encoding() await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) }) it("TimeZoneNamed", async () => { const schema = Schema.TimeZoneNamed const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London")) const encoding = asserts.encoding() await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London")) }) it("TimeZone", async () => { const schema = Schema.TimeZone const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) await decoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London")) const encoding = asserts.encoding() await encoding.succeed(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)) await encoding.succeed(DateTime.zoneMakeNamedUnsafe("Europe/London")) }) it("DateTimeZoned", async () => { const schema = Schema.DateTimeZoned const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" })) const encoding = asserts.encoding() await encoding.succeed(DateTime.makeZonedUnsafe("2021-01-01T00:00:00.000Z", { timeZone: "Europe/London" })) }) it("ReadonlySet", async () => { const schema = Schema.ReadonlySet(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) strictEqual(schema.value, Schema.FiniteFromString) strictEqual(schema.annotate({}).value, Schema.FiniteFromString) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(new Set(["1", "2", "3"]), new Set([1, 2, 3])) await decoding.fail(null, `Expected ReadonlySet, got null`) await decoding.fail( new Set(["1", "2", null]), `Expected string, got null at ["values"][2]` ) }) it("HashSet", async () => { const schema = Schema.HashSet(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) strictEqual(schema.value, Schema.FiniteFromString) strictEqual(schema.annotate({}).value, Schema.FiniteFromString) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(HashSet.make("1", "2", "3"), HashSet.make(1, 2, 3)) await decoding.fail(null, `Expected HashSet, got null`) await decoding.fail( HashSet.make(null), `Expected string, got null at ["values"][0]` ) const encoding = asserts.encoding() await encoding.succeed(HashSet.make(1, 2, 3), HashSet.make("1", "2", "3")) }) it("Chunk", async () => { const schema = Schema.Chunk(Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) strictEqual(schema.value, Schema.FiniteFromString) strictEqual(schema.annotate({}).value, Schema.FiniteFromString) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Chunk.make("1", "2", "3"), Chunk.make(1, 2, 3)) await decoding.fail(null, `Expected Chunk, got null`) await decoding.fail( Chunk.make(null), `Expected string, got null at ["values"][0]` ) const encoding = asserts.encoding() await encoding.succeed(Chunk.make(1, 2, 3), Chunk.make("1", "2", "3")) }) it("ReadonlyMap", async () => { const schema = Schema.ReadonlyMap(Schema.String, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) strictEqual(schema.key, Schema.String) strictEqual(schema.annotate({}).key, Schema.String) strictEqual(schema.value, Schema.FiniteFromString) strictEqual(schema.annotate({}).value, Schema.FiniteFromString) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(new Map([["a", "1"]]), new Map([["a", 1]])) await decoding.fail(null, `Expected ReadonlyMap, got null`) await decoding.fail( new Map([["a", null]]), `Expected string, got null at ["entries"][0][1]` ) const encoding = asserts.encoding() await encoding.succeed(new Map([["a", 1]]), new Map([["a", "1"]])) }) it("HashMap", async () => { const schema = Schema.HashMap(Schema.String, Schema.FiniteFromString) const asserts = new TestSchema.Asserts(schema) strictEqual(schema.key, Schema.String) strictEqual(schema.annotate({}).key, Schema.String) strictEqual(schema.value, Schema.FiniteFromString) strictEqual(schema.annotate({}).value, Schema.FiniteFromString) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(HashMap.make(["a", "1"]), HashMap.make(["a", 1])) await decoding.fail(null, `Expected HashMap, got null`) await decoding.fail( HashMap.make(["a", null]), `Expected string, got null at ["entries"][0][1]` ) const encoding = asserts.encoding() await encoding.succeed(HashMap.make(["a", 1]), HashMap.make(["a", "1"])) }) describe("Transformations", () => { it("toLowerCase", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String, SchemaTransformation.toLowerCase() ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("A", "a") await decoding.succeed("B", "b") }) it("toUpperCase", async () => { const schema = Schema.String.pipe( Schema.decodeTo(Schema.String, SchemaTransformation.toUpperCase()) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a", "A") await decoding.succeed("b", "B") }) }) describe("Opaque", () => { it("Struct", () => { class A extends Schema.Opaque()(Schema.Struct({ a: Schema.String })) {} const schema = A const instance = schema.make({ a: "a" }) strictEqual(instance.a, "a") deepStrictEqual(A.fields, { a: Schema.String }) }) }) describe("instanceOf", () => { it("arg: message: string", async () => { class MyError extends Error { constructor(message?: string) { super(message) this.name = "MyError" Object.setPrototypeOf(this, MyError.prototype) } } const schema = Schema.instanceOf( MyError, { expected: "MyError" } ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(new MyError("a")) await decoding.fail(null, `Expected MyError, got null`) const encoding = asserts.encoding() await encoding.succeed(new MyError("a")) await encoding.fail(null, `Expected MyError, got null`) }) }) it("Duration", async () => { const schema = Schema.Duration const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("DurationFromString", async () => { const schema = Schema.DurationFromString const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("1000 millis", Duration.millis(1000)) await decoding.succeed("1 second", Duration.seconds(1)) await decoding.succeed("Infinity", Duration.infinity) await decoding.succeed("-Infinity", Duration.negativeInfinity) await decoding.fail("value", "Invalid Duration string: value") const encoding = asserts.encoding() await encoding.succeed(Duration.zero, "0 millis") await encoding.succeed(Duration.seconds(5), "5000 millis") await encoding.succeed(Duration.nanos(5000n), "5000 nanos") await encoding.succeed(Duration.infinity, "Infinity") await encoding.succeed(Duration.negativeInfinity, "-Infinity") }) it("DurationFromNanos", async () => { const schema = Schema.DurationFromNanos const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(0n, Duration.zero) await decoding.succeed(1000n, Duration.nanos(1000n)) const encoding = asserts.encoding() await encoding.succeed(Duration.millis(5), 5_000_000n) await encoding.succeed(Duration.nanos(5000n), 5000n) await encoding.fail(Duration.infinity, "Unable to encode Infinity into a bigint") }) it("DurationFromMillis", async () => { const schema = Schema.DurationFromMillis const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(Infinity, Duration.infinity) await decoding.succeed(0, Duration.millis(0)) await decoding.succeed(1000, Duration.seconds(1)) await decoding.succeed(60 * 1000, Duration.minutes(1)) await decoding.succeed(0.1, Duration.millis(0.1)) await decoding.fail(-1, "Expected a value greater than or equal to 0, got -1") await decoding.fail(NaN, "Expected a value greater than or equal to 0, got NaN") const encoding = asserts.encoding() await encoding.succeed(Duration.infinity, Infinity) await encoding.succeed(Duration.millis(NaN), 0) await encoding.succeed(Duration.seconds(5), 5000) await encoding.succeed(Duration.millis(5000), 5000) await encoding.succeed(Duration.millis(0.1), 0.1) await encoding.succeed(Duration.nanos(5000n), 0.005) }) it("BigDecimal", async () => { const schema = Schema.BigDecimal const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("123.45")) await decoding.fail(null, `Expected BigDecimal, got null`) const encoding = asserts.encoding() await encoding.succeed(BigDecimal.fromStringUnsafe("123.45")) }) describe("BigDecimal checks", () => { it("isGreaterThanBigDecimal", async () => { const schema = Schema.BigDecimal.check(Schema.isGreaterThanBigDecimal(BigDecimal.fromStringUnsafe("1"))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("2")) await decoding.fail( BigDecimal.fromStringUnsafe("1"), `Expected a value greater than 1, got BigDecimal(1)` ) }) it("isGreaterThanOrEqualToBigDecimal", async () => { const schema = Schema.BigDecimal.check( Schema.isGreaterThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("1")) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("1")) await decoding.fail( BigDecimal.fromStringUnsafe("0"), `Expected a value greater than or equal to 1, got BigDecimal(0)` ) }) it("isLessThanBigDecimal", async () => { const schema = Schema.BigDecimal.check(Schema.isLessThanBigDecimal(BigDecimal.fromStringUnsafe("1"))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("0")) await decoding.fail( BigDecimal.fromStringUnsafe("1"), `Expected a value less than 1, got BigDecimal(1)` ) }) it("isLessThanOrEqualToBigDecimal", async () => { const schema = Schema.BigDecimal.check(Schema.isLessThanOrEqualToBigDecimal(BigDecimal.fromStringUnsafe("1"))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("1")) await decoding.fail( BigDecimal.fromStringUnsafe("2"), `Expected a value less than or equal to 1, got BigDecimal(2)` ) }) it("isBetweenBigDecimal", async () => { const schema = Schema.BigDecimal.check(Schema.isBetweenBigDecimal({ minimum: BigDecimal.fromStringUnsafe("1"), maximum: BigDecimal.fromStringUnsafe("5") })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(BigDecimal.fromStringUnsafe("3")) await decoding.fail( BigDecimal.fromStringUnsafe("0"), `Expected a value between 1 and 5, got BigDecimal(0)` ) }) }) describe("tag", () => { it("decoding: required & encoding: required & constructor: required", async () => { const schema = Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.FiniteFromString }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ _tag: "a", a: 1 }) const decoding = asserts.decoding() await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 }) const encoding = asserts.encoding() await encoding.succeed({ _tag: "a", a: 1 }, { _tag: "a", a: "1" }) }) it("decoding: required & encoding: required & constructor: optional", async () => { const schema = Schema.Struct({ _tag: Schema.tag("a"), a: Schema.FiniteFromString }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ _tag: "a", a: 1 }) await make.succeed({ a: 1 }, { _tag: "a", a: 1 }) const decoding = asserts.decoding() await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 }) const encoding = asserts.encoding() await encoding.succeed({ _tag: "a", a: 1 }, { _tag: "a", a: "1" }) }) it("decoding: default & encoding: omit & constructor: optional", async () => { const schema = Schema.Struct({ _tag: Schema.tag("a").pipe( Schema.encodeTo( Schema.optionalKey(Schema.Literal("a")), { decode: SchemaGetter.withDefault(Effect.succeed("a")), encode: SchemaGetter.omit() } ) ), a: Schema.FiniteFromString }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ _tag: "a", a: 1 }) await make.succeed({ a: 1 }, { _tag: "a", a: 1 }) const decoding = asserts.decoding() await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 }) await decoding.succeed({ a: "1" }, { _tag: "a", a: 1 }) const encoding = asserts.encoding() await encoding.succeed({ _tag: "a", a: 1 }, { a: "1" }) }) }) it("tagDefaultOmit", async () => { const schema = Schema.Struct({ _tag: Schema.tagDefaultOmit("a"), a: Schema.FiniteFromString }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ _tag: "a", a: 1 }) await make.succeed({ a: 1 }, { _tag: "a", a: 1 }) await make.fail( { _tag: "c", a: 1 }, `Expected "a", got "c" at ["_tag"]` ) const decoding = asserts.decoding() await decoding.succeed({ _tag: "a", a: "1" }, { _tag: "a", a: 1 }) await decoding.succeed({ a: "1" }, { _tag: "a", a: 1 }) await decoding.fail( { _tag: "c", a: 1 }, `Expected "a", got "c" at ["_tag"]` ) const encoding = asserts.encoding() await encoding.succeed({ _tag: "a", a: 1 }, { a: "1" }) await encoding.succeed({ a: 1 }, { a: "1" }) }) it("URL", async () => { const schema = Schema.URL const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("RegExp", async () => { const schema = Schema.RegExp const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("URLFromString", async () => { const schema = Schema.URLFromString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("https://effect.website", new URL("https://effect.website")) await decoding.fail( "123", `Invalid URL string: 123` ) const encoding = asserts.encoding() await encoding.succeed(new URL("https://effect.website"), "https://effect.website/") }) describe("UnknownFromJsonString / fromJsonString", () => { it("use case: Unknown <-> JSON string", async () => { const schema = Schema.UnknownFromJsonString const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(`{"a":1}`, { a: 1 }) await decoding.fail( `{"a"`, "SyntaxError: Expected ':' after property name in JSON at position 4 (line 1 column 5)" ) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, `{"a":1}`) }) it("use case: create a JSON string serializer for an existing schema", async () => { const schema = Schema.fromJsonString(Schema.Struct({ b: Schema.Number })) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(`{"b":1}`, { b: 1 }) await decoding.fail( `{"a":null}`, `Missing key at ["b"]` ) }) it("use case: parse / stringify a nested schema", async () => { const schema = Schema.Struct({ a: Schema.fromJsonString(Schema.Struct({ b: Schema.Number })) }) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed({ a: `{"b":2}` }, { a: { b: 2 } }) await decoding.fail( { a: `{"a":null}` }, `Missing key at ["a"]["b"]` ) }) }) it("fromFormData", async () => { const schema = Schema.fromFormData(Schema.Struct({ a: Schema.NonEmptyString, b: Schema.Struct({ c: Schema.String, d: Schema.String }), e: Schema.Array(Schema.String) })) const asserts = new TestSchema.Asserts(schema) const formData = new FormData() formData.append("a", "a") formData.append("b[c]", "bc") formData.append("b[d]", "bd") formData.append("e[0]", "e0") formData.append("e[1]", "e1") const decoded = { a: "a", b: { c: "bc", d: "bd" }, e: ["e0", "e1"] } const decoding = asserts.decoding() await decoding.succeed(formData, decoded) const encoding = asserts.encoding() { const formData = new FormData() formData.append("a", "a") formData.append("b[c]", "bc") formData.append("b[d]", "bd") formData.append("e", "e0") formData.append("e", "e1") await encoding.succeed(decoded, formData) } { const formData = new FormData() formData.append("a", "") await decoding.fail( formData, `Expected a value with a length of at least 1, got "" at ["a"]` ) } }) it("fromURLSearchParams", async () => { const schema = Schema.fromURLSearchParams(Schema.Struct({ a: Schema.NonEmptyString, b: Schema.Struct({ c: Schema.String, d: Schema.String }), e: Schema.Array(Schema.String) })) const asserts = new TestSchema.Asserts(schema) const urlSearchParams = new URLSearchParams("a=a&b[c]=bc&b[d]=bd&e=e0&e=e1") const decoded = { a: "a", b: { c: "bc", d: "bd" }, e: ["e0", "e1"] } const decoding = asserts.decoding() await decoding.succeed(urlSearchParams, decoded) const encoding = asserts.encoding() { const urlSearchParams = new URLSearchParams("a=a&b[c]=bc&b[d]=bd&e=e0&e=e1") await encoding.succeed(decoded, urlSearchParams) } { const urlSearchParams = new URLSearchParams("a=") await decoding.fail( urlSearchParams, `Expected a value with a length of at least 1, got "" at ["a"]` ) } }) it("Trim", async () => { const schema = Schema.Trim const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed("a ", "a") await decoding.succeed(" a", "a") await decoding.succeed(" a ", "a") await decoding.succeed("a\n", "a") const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "a ", `Expected a string with no leading or trailing whitespace, got "a "` ) }) it("transformOrFail", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String, SchemaTransformation.transformOrFail({ decode: (s) => s === "a" ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "a"` })) : Effect.succeed(s), encode: (s) => s === "b" ? Effect.fail(new SchemaIssue.Forbidden(Option.some(s), { message: `input should not be "b"` })) : Effect.succeed(s) }) ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("b") await decoding.fail( "a", `input should not be "a"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "b", `input should not be "b"` ) }) describe("TemplateLiteral", () => { it("should expose the parts", () => { const parts = ["a", Schema.String] as const const schema = Schema.TemplateLiteral(parts) deepStrictEqual(schema.parts, parts) }) it(`NonEmptyString + String`, async () => { const schema = Schema.TemplateLiteral([Schema.NonEmptyString, Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") }) it("rejects checks on Literal, TemplateLiteral, and Union parts", () => { const check = Schema.makeFilter(() => true) throws( () => Schema.TemplateLiteral([Schema.Literal("a").check(check)]), "Invalid TemplateLiteral part Literal" ) throws( () => Schema.TemplateLiteral([Schema.TemplateLiteral(["a"]).check(check)]), "Invalid TemplateLiteral part TemplateLiteral" ) throws( () => Schema.TemplateLiteral([Schema.Union([Schema.Literal("a"), Schema.Literal("b")]).check(check)]), "Invalid TemplateLiteral part Union" ) }) it(`"a"`, async () => { const schema = Schema.TemplateLiteral(["a"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(null, "Expected string, got null") await decoding.fail( "ab", `Expected a string matching template literal parts, got "ab"` ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) }) it(`"a b"`, async () => { const schema = Schema.TemplateLiteral(["a", " ", "b"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a b") await decoding.fail( "a b", `Expected a string matching template literal parts, got "a b"` ) }) it(`"[" + string + "]"`, async () => { const schema = Schema.TemplateLiteral(["[", Schema.String, "]"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("[a]") await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) }) it(`"a" + string`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed("ab") await decoding.fail( null, "Expected string, got null" ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) }) it(`"a" + number`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.Number]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a1") await decoding.succeed("a+1") await decoding.succeed("a1.2") await decoding.succeed("a-1.401298464324817e-45") await decoding.succeed("a1.401298464324817e-45") await decoding.succeed("a+1.401298464324817e-45") await decoding.succeed("a-1.401298464324817e+45") await decoding.succeed("a1.401298464324817e+45") await decoding.succeed("a+1.401298464324817e+45") await decoding.succeed("a-1.401298464324817E-45") await decoding.succeed("a1.401298464324817E-45") await decoding.succeed("a+1.401298464324817E-45") await decoding.succeed("a-1.401298464324817E+45") await decoding.succeed("a1.401298464324817E+45") await decoding.succeed("a+1.401298464324817E+45") await decoding.fail( null, "Expected string, got null" ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( "aa", `Expected a string matching template literal parts, got "aa"` ) }) it(`"a" + bigint`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.BigInt]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a0") await decoding.succeed("a1") await decoding.succeed("a-1") await decoding.fail( null, "Expected string, got null" ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( "aa", `Expected a string matching template literal parts, got "aa"` ) await decoding.fail( "a1.2", `Expected a string matching template literal parts, got "a1.2"` ) await decoding.fail( "a+1", `Expected a string matching template literal parts, got "a+1"` ) }) it(`string`, async () => { const schema = Schema.TemplateLiteral([Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed("ab") await decoding.succeed("") await decoding.succeed("\n") await decoding.succeed("\r") await decoding.succeed("\r\n") await decoding.succeed("\t") }) it(`\\n + string`, async () => { const schema = Schema.TemplateLiteral(["\n", Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("\n") await decoding.succeed("\na") await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) }) it(`a\\nb + string`, async () => { const schema = Schema.TemplateLiteral(["a\nb ", Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a\nb ") await decoding.succeed("a\nb c") }) it(`"a" + string + "b"`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.String, "b"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ab") await decoding.succeed("acb") await decoding.succeed("abb") await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) await decoding.fail( "b", `Expected a string matching template literal parts, got "b"` ) const encoding = asserts.encoding() await encoding.succeed("acb") }) it(`"a" + string + "b" + string`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.String, "b", Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ab") await decoding.succeed("acb") await decoding.succeed("acbd") await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) await decoding.fail( "b", `Expected a string matching template literal parts, got "b"` ) }) it("https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html", async () => { const EmailLocaleIDs = Schema.Literals(["welcome_email", "email_heading"]) const FooterLocaleIDs = Schema.Literals(["footer_title", "footer_sendoff"]) const schema = Schema.TemplateLiteral([Schema.Union([EmailLocaleIDs, FooterLocaleIDs]), "_id"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("welcome_email_id") await decoding.succeed("email_heading_id") await decoding.succeed("footer_title_id") await decoding.succeed("footer_sendoff_id") await decoding.fail( "_id", `Expected a string matching template literal parts, got "_id"` ) }) it(`string + 0`, async () => { const schema = Schema.TemplateLiteral([Schema.String, 0]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a0") await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) }) it(`string + 1n`, async () => { const schema = Schema.TemplateLiteral([Schema.String, 1n]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a1") await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) }) it(`string + ("a" | 0)`, async () => { const schema = Schema.TemplateLiteral([Schema.String, Schema.Literals(["a", 0])]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a0") await decoding.succeed("aa") await decoding.fail( "b", `Expected a string matching template literal parts, got "b"` ) }) it(`(string | 1) + (number | true)`, async () => { const schema = Schema.TemplateLiteral([ Schema.Union([Schema.String, Schema.Literal(1)]), Schema.Union([Schema.Number, Schema.Literal("true")]) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("atrue") await decoding.succeed("-2") await decoding.succeed("10.1") await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) }) it("`c${`a${string}b` | \"e\"}d`", async () => { const schema = Schema.TemplateLiteral( ["c", Schema.Union([Schema.TemplateLiteral(["a", Schema.String, "b"]), Schema.Literal("e")]), "d"] ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ced") await decoding.succeed("cabd") await decoding.succeed("casbd") await decoding.succeed("ca bd") await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) }) it("< + h + (1|2n) + >", async () => { const schema = Schema.TemplateLiteral(["<", Schema.TemplateLiteral(["h", Schema.Literals([1, 2n])]), ">"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("

") await decoding.succeed("

") await decoding.fail( "

", `Expected a string matching template literal parts, got "

"` ) }) it(`"a" + check`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.NonEmptyString]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ab") await decoding.fail( null, "Expected string, got null" ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( "a", `Expected a string matching template literal parts, got "a"` ) }) it(`"a" + transformation`, async () => { const schema = Schema.TemplateLiteral(["a", Schema.FiniteFromString]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed("a1") await decoding.fail( null, "Expected string, got null" ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( "ab", `Expected a finite number, got NaN at [1]` ) }) }) describe("TemplateLiteralParser", () => { it("should expose the parts", () => { const parts = ["a", Schema.String] as const const schema = Schema.TemplateLiteralParser(parts) deepStrictEqual(schema.parts, parts) }) it(`NonEmptyString + String`, async () => { const schema = Schema.TemplateLiteralParser([Schema.NonEmptyString, Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a", ["a", ""]) }) it(`"a"`, async () => { const schema = Schema.TemplateLiteralParser(["a"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a", ["a"]) await decoding.fail( "ab", `Expected a string matching template literal parts, got "ab"` ) await decoding.fail( "", `Expected a string matching template literal parts, got ""` ) await decoding.fail( null, "Expected string, got null" ) }) it(`"a b"`, async () => { const schema = Schema.TemplateLiteralParser(["a", " ", "b"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a b", ["a", " ", "b"]) await decoding.fail( "a b", `Expected a string matching template literal parts, got "a b"` ) }) it(`Int + "a"`, async () => { const schema = Schema.TemplateLiteralParser([Schema.Int, "a"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("1a", [1, "a"]) await decoding.fail( "1.1a", `Expected a string matching template literal parts, got "1.1a"` ) const encoding = asserts.encoding() await encoding.succeed([1, "a"], "1a") await encoding.fail( [1.1, "a"], `Expected an integer, got 1.1 at [0]` ) }) it(`Int + String`, async () => { const schema = Schema.TemplateLiteralParser([Schema.Number.check(Schema.isInt()), Schema.String]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("1.2", [1, ".2"]) }) it(`NumberFromString + "a" + NonEmptyString`, async () => { const schema = Schema.TemplateLiteralParser([Schema.FiniteFromString, "a", Schema.NonEmptyString]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("100ab", [100, "a", "b"]) await decoding.succeed("100ab23a", [100, "a", "b23a"]) await decoding.fail( "-ab", `Expected a finite number, got NaN at [0]` ) const encoding = asserts.encoding() await encoding.succeed([100, "a", "b"], "100ab") await encoding.fail( [100, "a", ""], `Expected a value with a length of at least 1, got "" at [2]` ) }) it(`"h" + (1 | 2 | 3)`, async () => { const schema = Schema.TemplateLiteralParser(["h", Schema.Literals([1, 2, 3])]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("h1", ["h", 1]) }) it(`"c" + (\`a\${string}b\`|"e") + "d"`, async () => { const schema = Schema.TemplateLiteralParser([ "c", Schema.Union([Schema.TemplateLiteralParser(["a", Schema.NonEmptyString, "b"]), Schema.Literal("e")]), "d" ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ca bd", ["c", ["a", " ", "b"], "d"]) await decoding.succeed("ced", ["c", "e", "d"]) await decoding.fail( "cabd", `Expected a string matching template literal parts, got "ab" at [1]` ) await decoding.fail( "ed", `Expected a string matching template literal parts, got "ed"` ) }) it(`"c" + (\`a\${number}b\`|"e") + "d"`, async () => { const schema = Schema.TemplateLiteralParser([ "c", Schema.Union([ Schema.TemplateLiteralParser(["a", Schema.Finite.check(Schema.isInt()), "b"]), Schema.Literal("e") ]), "d" ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("ced", ["c", "e", "d"]) await decoding.succeed("ca1bd", ["c", ["a", 1, "b"], "d"]) await decoding.fail( "ca1.1bd", `Expected a string matching template literal parts, got "a1.1b" at [1]` ) await decoding.fail( "ca-bd", `Expected a string matching template literal parts, got "a-b" at [1]` ) }) it(`readonly ["<", \`h\${1 | 2}\`, ">"]`, async () => { const schema = Schema.TemplateLiteralParser(["<", Schema.TemplateLiteral(["h", Schema.Literals([1, 2])]), ">"]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("

", ["<", "h1", ">"]) await decoding.succeed("

", ["<", "h2", ">"]) await decoding.fail( "

", `Expected a string matching template literal parts, got "

"` ) }) it(`readonly ["<", readonly ["h", 1 | 2], ">"]`, async () => { const schema = Schema.TemplateLiteralParser([ "<", Schema.TemplateLiteralParser(["h", Schema.Literals([1, 2])]), ">" ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("

", ["<", ["h", 1], ">"]) await decoding.succeed("

", ["<", ["h", 2], ">"]) await decoding.fail( "

", `Expected a string matching template literal parts, got "h3" at [1]` ) }) }) describe("Class", () => { it("make with void input", () => { class A extends Schema.Class("A")({}) {} deepStrictEqual(A.make(), new A()) deepStrictEqual(A.makeOption(), Option.some(new A())) deepStrictEqual(Effect.runSync(A.makeEffect()), new A()) }) it("suspend before initialization", async () => { const schema = Schema.suspend(() => string) class A extends Schema.Class("A")(Schema.Struct({ a: schema })) {} const string = Schema.String const asserts = new TestSchema.Asserts(A) const make = asserts.make() await make.succeed(new A({ a: "a" })) await make.succeed({ a: "a" }, new A({ a: "a" })) const decoding = asserts.decoding() await decoding.succeed(new A({ a: "a" })) }) it("should memoize the ast", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} assertTrue(A.ast === A.ast) }) it("should set the identifier annotation", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} strictEqual(A.ast.annotations?.identifier, "A") }) describe("should be compatible with `immer`", () => { it("`[immerable]`", () => { class A extends Schema.Class("A")({ a: Schema.Struct({ b: Schema.FiniteFromString }).pipe(Schema.optional), c: Schema.FiniteFromString }) {} const a = new A({ a: { b: 1 }, c: 2 }) const modified = produce(a, (draft) => { if (draft.a) { draft.a.b = 2 } }) assertTrue(modified instanceof A) strictEqual(modified.a?.b, 2) strictEqual(modified.c, 2) strictEqual(a.a?.b, 1) }) it("Equality", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} const a = new A({ a: "a" }) const a1 = produce(a, (draft) => { draft.a = "a1" }) const a2 = produce(a, (draft) => { draft.a = "a1" }) assertTrue(Equal.equals(a1, new A({ a: "a1" }))) assertTrue(Equal.equals(a1, a2)) }) }) it("mapFields", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} const schema = A.mapFields((fields) => ({ ...fields, b: Schema.Number })) deepStrictEqual(schema.fields, { a: Schema.String, b: Schema.Number }) }) it("Struct with nested Class", async () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) {} const schema = Schema.Struct({ a: A.pipe(Schema.withConstructorDefault(Effect.succeed(new A({ a: "default" })))) }) const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: new A({ a: "a" }) }) await make.succeed({}, { a: new A({ a: "default" }) }) }) it("Class with nested Class", async () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) {} class B extends Schema.Class("B")(Schema.Struct({ a: A.pipe(Schema.withConstructorDefault(Effect.succeed(new A({ a: "default" })))) })) {} const schema = B const asserts = new TestSchema.Asserts(schema) const make = asserts.make() await make.succeed({ a: new A({ a: "a" }) }, new B({ a: new A({ a: "a" }) })) await make.succeed({}, new B({ a: new A({ a: "default" }) })) }) it("should be possible to define a class with a mutable field", async () => { class A extends Schema.Class("A")({ a: Schema.mutableKey(Schema.String) }) { public update() { this.a = "b" } } const asserts = new TestSchema.Asserts(A) const make = asserts.make() await make.succeed(new A({ a: "a" })) await make.succeed({ a: "a" }, new A({ a: "a" })) const a = new A({ a: "a" }) a.update() strictEqual(a.a, "b") }) it("Fields argument", async () => { class A extends Schema.Class("A")({ a: Schema.String }) { readonly _a = 1 } const asserts = new TestSchema.Asserts(A) // should be a schema assertTrue(Schema.isSchema(A)) // should expose the fields deepStrictEqual(A.fields, { a: Schema.String }) // should expose the identifier strictEqual(A.identifier, "A") strictEqual(A.name, "A") assertTrue(new A({ a: "a" }) instanceof A) assertTrue(A.make({ a: "a" }) instanceof A) // test additional fields strictEqual(new A({ a: "a" })._a, 1) strictEqual(A.make({ a: "a" })._a, 1) // test Equal.equals assertTrue(Equal.equals(new A({ a: "a" }), new A({ a: "a" }))) assertFalse(Equal.equals(new A({ a: "a" }), new A({ a: "b" }))) const make = asserts.make() await make.succeed(new A({ a: "a" })) await make.succeed({ a: "a" }, new A({ a: "a" })) const decoding = asserts.decoding() await decoding.succeed({ a: "a" }, new A({ a: "a" })) await decoding.fail( null, `Expected object, got null` ) await decoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed(new A({ a: "a" }), { a: "a" }) await encoding.fail( null, "Expected A, got null" ) await encoding.fail( { a: "a" }, `Expected A, got {"a":"a"}` ) }) it("Struct argument", async () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) { readonly _a = 1 } const asserts = new TestSchema.Asserts(A) // should be a schema assertTrue(Schema.isSchema(A)) // should expose the fields deepStrictEqual(A.fields, { a: Schema.String }) // should expose the identifier strictEqual(A.identifier, "A") strictEqual(A.name, "A") assertTrue(new A({ a: "a" }) instanceof A) assertTrue(A.make({ a: "a" }) instanceof A) // test additional fields strictEqual(new A({ a: "a" })._a, 1) strictEqual(A.make({ a: "a" })._a, 1) // test Equal.equals assertTrue(Equal.equals(new A({ a: "a" }), new A({ a: "a" }))) assertFalse(Equal.equals(new A({ a: "a" }), new A({ a: "b" }))) const make = asserts.make() await make.succeed(new A({ a: "a" })) await make.succeed({ a: "a" }, new A({ a: "a" })) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed({ a: "a" }, new A({ a: "a" })) await decoding.fail( { a: 1 }, `Expected string, got 1 at ["a"]` ) const encoding = asserts.encoding() await encoding.succeed(new A({ a: "a" }), { a: "a" }) await encoding.fail( null, "Expected A, got null" ) await encoding.fail( { a: "a" }, `Expected A, got {"a":"a"}` ) }) it("constructor ignores excess properties by default", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} const instance = new A({ a: "a", extra: "extra" } as any) strictEqual(instance.a, "a") assertFalse("extra" in instance) }) it("constructor preserves excess properties when requested", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} const instance = new A({ a: "a", extra: "extra" } as any, { parseOptions: { onExcessProperty: "preserve" } }) strictEqual(instance.a, "a") strictEqual((instance as any).extra, "extra") }) it("constructor rejects excess properties when requested", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} throws(() => new A({ a: "a", extra: "extra" } as any, { parseOptions: { onExcessProperty: "error" } }) ) }) it("annotate", async () => { class A extends Schema.Class("A")({ a: Schema.String }) {} const Annotated = A.annotate({}) // should be a schema assertTrue(Schema.isSchema(Annotated)) // should expose the fields deepStrictEqual(Annotated.from.fields, { a: Schema.String }) assertTrue(Annotated.make(new A({ a: "a" })) instanceof A) }) describe("extend", () => { it("basic", async () => { class A extends Schema.Class("A")(Schema.Struct({ a: Schema.String })) { readonly _a = 1 } class B extends A.extend("B")({ b: Schema.Number }) { readonly _b = 2 } const asserts = new TestSchema.Asserts(B) const instance = new B({ a: "a", b: 2 }) assertTrue(instance instanceof A) assertTrue(B.make({ a: "a", b: 2 }) instanceof A) assertTrue(instance instanceof B) assertTrue(B.make({ a: "a", b: 2 }) instanceof B) strictEqual(instance.a, "a") strictEqual(instance._a, 1) strictEqual(instance.b, 2) strictEqual(instance._b, 2) const make = asserts.make() await make.succeed(new B({ a: "a", b: 2 })) await make.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) }) it("constructor preserves subclass fields while ignoring excess properties by default", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2, extra: "extra" } as any) strictEqual(instance.a, "a") strictEqual(instance.b, 2) assertFalse("extra" in instance) }) it("constructor preserves subclass fields and excess properties when requested", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2, extra: "extra" } as any, { parseOptions: { onExcessProperty: "preserve" } }) strictEqual(instance.a, "a") strictEqual(instance.b, 2) strictEqual((instance as any).extra, "extra") }) it("constructor does not treat subclass fields as excess properties", () => { class A extends Schema.Class("A")({ a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2 }, { parseOptions: { onExcessProperty: "error" } }) strictEqual(instance.a, "a") strictEqual(instance.b, 2) throws(() => new B({ a: "a", b: 2, extra: "extra" } as any, { parseOptions: { onExcessProperty: "error" } }) ) }) it("Struct argument", async () => { class A extends Schema.Class("A")( Schema.Struct({ a: Schema.Number }).check(Schema.makeFilter(({ a }) => a > 0, { expected: "positive a" })) ) {} class B extends A.extend("B")( Schema.Struct({ b: Schema.Number }).check(Schema.makeFilter(({ b }) => b > 0, { expected: "positive b" })) ) {} const asserts = new TestSchema.Asserts(B) const make = asserts.make() await make.succeed({ a: 1, b: 1 }, new B({ a: 1, b: 1 })) await make.fail({ a: 0, b: 1 }, `Expected positive a, got {"a":0,"b":1}`) await make.fail({ a: 1, b: 0 }, `Expected positive b, got {"a":1,"b":0}`) }) it("static members", async () => { class A extends Schema.Class("A")({ a: Schema.String }) { static readonly aStatic = "value" } class B extends A.extend("B")({ b: Schema.Number }) {} strictEqual(B.aStatic, "value") }) }) }) describe("TaggedClass", () => { it("make with void input", () => { class A extends Schema.TaggedClass()("A", {}) {} deepStrictEqual(A.make(), new A()) deepStrictEqual(A.makeOption(), Option.some(new A())) deepStrictEqual(Effect.runSync(A.makeEffect()), new A()) }) it("explicit identifier", async () => { class A extends Schema.TaggedClass("B")("A", { a: Schema.String }) {} strictEqual(A.identifier, "B") }) it("fields argument", async () => { class A extends Schema.TaggedClass()("A", { a: Schema.String }) {} const asserts = new TestSchema.Asserts(A) assertTrue(Schema.isSchema(A)) deepStrictEqual(Object.keys(A.fields).sort(), ["_tag", "a"]) strictEqual(A.identifier, "A") const instance = new A({ a: "a" }) strictEqual(instance._tag, "A") strictEqual(instance.a, "a") assertTrue(instance instanceof A) const make = asserts.make() await make.succeed(new A({ a: "a" })) await make.succeed({ a: "a" }, new A({ a: "a" })) const decoding = asserts.decoding() await decoding.succeed({ _tag: "A", a: "a" }, new A({ a: "a" })) const encoding = asserts.encoding() await encoding.succeed(new A({ a: "a" }), { _tag: "A", a: "a" }) }) it("constructor ignores excess properties by default", () => { class A extends Schema.TaggedClass()("A", { a: Schema.String }) {} const instance = new A({ a: "a", extra: "extra" } as any) strictEqual(instance._tag, "A") strictEqual(instance.a, "a") assertFalse("extra" in instance) }) it("Struct argument", async () => { class A extends Schema.TaggedClass()( "A", Schema.Struct({ a: Schema.String }).check(Schema.makeFilter(({ a }) => a.length > 0, { expected: `"a" being longer than 0` })) ) {} const asserts = new TestSchema.Asserts(A) const make = asserts.make() await make.succeed({ a: "a" }, new A({ a: "a" })) await make.fail({ a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`) const decoding = asserts.decoding() await decoding.succeed({ _tag: "A", a: "a" }, new A({ a: "a" })) await decoding.fail( { a: "a" }, `Missing key at ["_tag"]` ) await decoding.fail({ _tag: "A", a: "" }, `Expected "a" being longer than 0, got {"_tag":"A","a":""}`) }) it("extended constructor does not treat subclass fields as excess properties", () => { class A extends Schema.TaggedClass()("A", { a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2, extra: "extra" } as any) strictEqual(instance._tag, "A") strictEqual(instance.a, "a") strictEqual(instance.b, 2) assertFalse("extra" in instance) const strictInstance = new B({ a: "a", b: 2 }, { parseOptions: { onExcessProperty: "error" } }) strictEqual(strictInstance._tag, "A") strictEqual(strictInstance.a, "a") strictEqual(strictInstance.b, 2) throws(() => new B({ a: "a", b: 2, extra: "extra" } as any, { parseOptions: { onExcessProperty: "error" } }) ) }) }) describe("ErrorClass", () => { it("make with void input", () => { class E extends Schema.ErrorClass("E")({}) {} deepStrictEqual(E.make(), new E()) deepStrictEqual(E.makeOption(), Option.some(new E())) deepStrictEqual(Effect.runSync(E.makeEffect()), new E()) }) it("fields argument", async () => { class E extends Schema.ErrorClass("E")({ id: Schema.Number }) {} const asserts = new TestSchema.Asserts(E) const err = new E({ id: 1 }) strictEqual(String(err), `E`) assertInclude(err.stack, "Schema.test.ts:") strictEqual(err.id, 1) const make = asserts.make() await make.succeed(new E({ id: 1 })) await make.succeed({ id: 1 }, new E({ id: 1 })) }) it("constructor ignores excess properties by default", () => { class E extends Schema.ErrorClass("E")({ message: Schema.String, cause: Schema.optionalKey(Schema.Unknown), code: Schema.Number }) {} const cause = new Error("cause") const err = new E({ message: "boom", cause, code: 1, extra: "extra" } as any) strictEqual(err.message, "boom") strictEqual(err.cause, cause) strictEqual(err.code, 1) assertFalse("extra" in err) }) it("constructor preserves excess properties when requested", () => { class E extends Schema.ErrorClass("E")({ message: Schema.String, code: Schema.Number }) {} const err = new E({ message: "boom", code: 1, extra: "extra" } as any, { parseOptions: { onExcessProperty: "preserve" } }) strictEqual(err.message, "boom") strictEqual(err.code, 1) strictEqual((err as any).extra, "extra") }) it("Struct argument", async () => { class E extends Schema.ErrorClass("E")(Schema.Struct({ id: Schema.Number })) {} const asserts = new TestSchema.Asserts(E) const err = new E({ id: 1 }) strictEqual(String(err), `E`) assertInclude(err.stack, "Schema.test.ts:") strictEqual(err.id, 1) const make = asserts.make() await make.succeed(new E({ id: 1 })) await make.succeed({ id: 1 }, new E({ id: 1 })) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } }) it("extend", async () => { class A extends Schema.ErrorClass("A")({ a: Schema.String }) { readonly _a = 1 } class B extends A.extend("B")({ b: Schema.Number }) { readonly _b = 2 } const asserts = new TestSchema.Asserts(B) const instance = new B({ a: "a", b: 2 }) strictEqual(String(instance), `B`) assertInclude(instance.stack, "Schema.test.ts:") assertTrue(instance instanceof A) assertTrue(B.make({ a: "a", b: 2 }) instanceof A) assertTrue(instance instanceof B) assertTrue(B.make({ a: "a", b: 2 }) instanceof B) strictEqual(instance.a, "a") strictEqual(instance._a, 1) strictEqual(instance.b, 2) strictEqual(instance._b, 2) const make = asserts.make() await make.succeed(new B({ a: "a", b: 2 })) await make.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 2 }, new B({ a: "a", b: 2 })) }) it("extended constructor ignores excess properties by default", () => { class A extends Schema.ErrorClass("A")({ message: Schema.String }) {} class B extends A.extend("B")({ code: Schema.Number }) {} const err = new B({ message: "boom", code: 1, extra: "extra" } as any) strictEqual(err.message, "boom") strictEqual(err.code, 1) assertFalse("extra" in err) }) it("extended constructor does not treat subclass fields as excess properties", () => { class A extends Schema.ErrorClass("A")({ message: Schema.String }) {} class B extends A.extend("B")({ code: Schema.Number }) {} const err = new B({ message: "boom", code: 1 }, { parseOptions: { onExcessProperty: "error" } }) strictEqual(err.message, "boom") strictEqual(err.code, 1) }) it("`toString` to match native `Error` output format", async () => { class E extends Schema.ErrorClass("E")({ message: Schema.String }) {} const err = new E({ message: "my message" }) strictEqual(String(err), `E: my message`) }) }) describe("TaggedErrorClass", () => { it("make with void input", () => { class E extends Schema.TaggedErrorClass()("E", {}) {} deepStrictEqual(E.make(), new E()) deepStrictEqual(E.makeOption(), Option.some(new E())) deepStrictEqual(Effect.runSync(E.makeEffect()), new E()) }) it("fields argument", async () => { class E extends Schema.TaggedErrorClass()("E", { id: Schema.Number }) {} const asserts = new TestSchema.Asserts(E) const err = new E({ id: 1 }) strictEqual(err._tag, "E") strictEqual(err.id, 1) assertInclude(err.stack, "Schema.test.ts:") const make = asserts.make() await make.succeed(new E({ id: 1 })) await make.succeed({ id: 1 }, new E({ id: 1 })) const decoding = asserts.decoding() await decoding.succeed({ _tag: "E", id: 1 }, new E({ id: 1 })) const encoding = asserts.encoding() await encoding.succeed(new E({ id: 1 }), { _tag: "E", id: 1 }) }) it("constructor ignores excess properties by default", () => { class E extends Schema.TaggedErrorClass()("E", { id: Schema.Number }) {} const err = new E({ id: 1, extra: "extra" } as any) strictEqual(err._tag, "E") strictEqual(err.id, 1) assertFalse("extra" in err) }) it("Struct argument", async () => { class E extends Schema.TaggedErrorClass()( "E", Schema.Struct({ id: Schema.Number }) ) {} const err = new E({ id: 1 }) strictEqual(err._tag, "E") strictEqual(err.id, 1) deepStrictEqual(Object.keys(E.fields).sort(), ["_tag", "id"]) }) it("name matches tag", () => { class E extends Schema.TaggedErrorClass()("TaggedErrorName", { id: Schema.Number }) {} const err = new E({ id: 1 }) strictEqual(err.name, "TaggedErrorName") }) it("name matches identifier", () => { class E extends Schema.TaggedErrorClass("A")("B", { a: Schema.Number }) {} const err = new E({ a: 1 }) strictEqual(err.name, "A") }) it("name matches identifier after extend", () => { class E extends Schema.TaggedErrorClass("A")("B", { a: Schema.Number }) {} class E2 extends E.extend("C")({ b: Schema.String }) {} const err = new E2({ a: 1, b: "b" }) strictEqual(err.name, "C") }) it("zero-field TaggedErrorClass allows omitting props argument", () => { class NotFoundError extends Schema.TaggedErrorClass()("NotFoundError", {}) {} // new NotFoundError() should work without passing {} const a = new NotFoundError() strictEqual(a._tag, "NotFoundError") assertTrue(a instanceof NotFoundError) // new NotFoundError({}) should also still work const b = new NotFoundError({}) strictEqual(b._tag, "NotFoundError") assertTrue(b instanceof NotFoundError) }) it("extend", async () => { class A extends Schema.TaggedErrorClass()("A", { a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2 }) strictEqual(instance._tag, "A") assertTrue(instance instanceof A) assertTrue(instance instanceof B) }) it("extended constructor ignores excess properties by default", () => { class A extends Schema.TaggedErrorClass()("A", { a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2, extra: "extra" } as any) strictEqual(instance._tag, "A") strictEqual(instance.a, "a") strictEqual(instance.b, 2) assertFalse("extra" in instance) }) it("extended constructor does not treat subclass fields as excess properties", () => { class A extends Schema.TaggedErrorClass()("A", { a: Schema.String }) {} class B extends A.extend("B")({ b: Schema.Number }) {} const instance = new B({ a: "a", b: 2 }, { parseOptions: { onExcessProperty: "error" } }) strictEqual(instance._tag, "A") strictEqual(instance.a, "a") strictEqual(instance.b, 2) }) }) describe("Enum", () => { it("enums should be exposed", () => { enum Fruits { Apple, Banana } const schema = Schema.Enum(Fruits) strictEqual(schema.enums.Apple, 0) strictEqual(schema.enums.Banana, 1) }) it("Numeric enum", async () => { enum Fruits { Apple, Banana } const schema = Schema.Enum(Fruits) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(Fruits.Apple) await decoding.succeed(Fruits.Banana) await decoding.succeed(0) await decoding.succeed(1) await decoding.fail( 3, `Expected 0 | 1, got 3` ) const encoding = asserts.encoding() await encoding.succeed(Fruits.Apple, 0) await encoding.succeed(Fruits.Banana, 1) }) it("String enum", async () => { enum Fruits { Apple = "apple", Banana = "banana", Cantaloupe = 0 } const schema = Schema.Enum(Fruits) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(Fruits.Apple) await decoding.succeed(Fruits.Cantaloupe) await decoding.succeed("apple") await decoding.succeed("banana") await decoding.succeed(0) await decoding.succeed(0) await decoding.fail( "Cantaloupe", `Expected "apple" | "banana" | 0, got "Cantaloupe"` ) const encoding = asserts.encoding() await encoding.succeed(Fruits.Apple) await encoding.succeed(Fruits.Banana) await encoding.succeed(Fruits.Cantaloupe) }) it("Const enum", async () => { const Fruits = { Apple: "apple", Banana: "banana", Cantaloupe: 3 } as const const schema = Schema.Enum(Fruits) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("apple") await decoding.succeed("banana") await decoding.succeed(3) await decoding.fail( "Cantaloupe", `Expected "apple" | "banana" | 3, got "Cantaloupe"` ) const encoding = asserts.encoding() await encoding.succeed(Fruits.Apple, "apple") await encoding.succeed(Fruits.Banana, "banana") await encoding.succeed(Fruits.Cantaloupe, 3) }) }) describe("catchDecoding", () => { it("sync fallback", async () => { const fallback = Effect.succeed(Option.some("b")) const schema = Schema.String.pipe(Schema.catchDecoding(() => fallback)).check(Schema.isNonEmpty()) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(null, "b") await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( null, "Expected string, got null" ) }) it("async fallback", async () => { const fallback = Effect.succeed(Option.some("b")).pipe(Effect.delay(100)) const schema = Schema.String.pipe(Schema.catchDecoding(() => fallback)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.succeed(null, "b") }) }) it("catchDecodingWithContext", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.String.pipe(Schema.catchDecodingWithContext(() => Effect.gen(function*() { const service = yield* Service return Option.some(yield* service.fallback) }) )) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide( Service, { fallback: Effect.succeed("b") } ) await decoding.succeed("a") await decoding.succeed(null, "b") }) describe("middlewareDecoding", () => { it("providing a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.FiniteFromString.pipe( Schema.catchDecodingWithContext((issue) => Effect.gen(function*() { if (issue._tag === "Encoding" && issue.issue._tag === "InvalidType") { return yield* Effect.fail(issue) } const service = yield* Service return Option.some(yield* service.fallback) }) ), Schema.middlewareDecoding(Effect.provideService(Service, { fallback: Effect.succeed(0) })) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("1", 1) await decoding.succeed("a", 0) await decoding.fail(null, "Expected string, got null") }) it("forced failure", async () => { const schema = Schema.String.pipe( Schema.middlewareDecoding(() => Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" })) ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "1", "my message" ) }) }) describe("catchEncoding", () => { it("sync fallback", async () => { const fallback = Effect.succeed(Option.some(0)) const schema = Schema.Number.pipe(Schema.catchEncoding(() => fallback), Schema.encodeTo(Schema.Int)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 1.2, `Expected an integer, got 1.2` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.succeed(null, 0) await encoding.fail( 1.2, `Expected an integer, got 1.2` ) }) it("async fallback", async () => { const fallback = Effect.succeed(Option.some(0)).pipe(Effect.delay(100)) const schema = Schema.Number.pipe(Schema.catchEncoding(() => fallback), Schema.encodeTo(Schema.Int)) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.succeed(null, 0) await encoding.fail( 1.2, `Expected an integer, got 1.2` ) }) }) it("catchEncodingWithContext", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Number.pipe( Schema.catchEncodingWithContext(() => Effect.gen(function*() { const service = yield* Service return Option.some(yield* service.fallback) }) ), Schema.encodeTo(Schema.Int) ) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding().provide( Service, { fallback: Effect.succeed(0) } ) await encoding.succeed(1) await encoding.succeed(null, 0) await encoding.fail( 1.2, `Expected an integer, got 1.2` ) }) describe("middlewareEncoding", () => { it("providing a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.FiniteFromString.pipe( Schema.catchEncodingWithContext((issue) => Effect.gen(function*() { if (issue._tag === "InvalidType") { return yield* Effect.fail(issue) } const service = yield* Service return Option.some(yield* service.fallback) }) ), Schema.middlewareEncoding(Effect.provideService(Service, { fallback: Effect.succeed("b") })) ) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed(1, "1") await encoding.succeed(NaN, "b") await encoding.fail(null, "Expected number, got null") }) it("forced failure", async () => { const schema = Schema.String.pipe( Schema.middlewareEncoding(() => Effect.fail(new SchemaIssue.Forbidden(Option.none(), { message: "my message" })) ) ) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.fail(1, "my message") }) }) describe("Optional Fields", () => { it("Exact Optional Property", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) }) it("Optional Property", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.FiniteFromString) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.succeed({ a: undefined }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) await encoding.succeed({ a: undefined }) }) it("Exact Optional Property with Nullability", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.NullOr(Schema.FiniteFromString)) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.succeed({ a: null }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) await encoding.succeed({ a: null }) }) it("Optional Property with Nullability", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.NullOr(Schema.FiniteFromString)) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.succeed({ a: undefined }) await decoding.succeed({ a: null }) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) await encoding.succeed({ a: null }) await encoding.succeed({ a: undefined }) }) it("Optional Property to Exact Optional Property", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.FiniteFromString).pipe(Schema.decodeTo(Schema.optionalKey(Schema.Number), { decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotUndefined)), encode: SchemaGetter.passthrough() })) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.succeed({ a: undefined }, {}) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) }) it("Optional Property with Nullability to Optional Property", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.NullOr(Schema.FiniteFromString)).pipe( Schema.decodeTo(Schema.optional(Schema.Number), { decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)), encode: SchemaGetter.passthrough() }) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: 1 }) await decoding.succeed({}) await decoding.succeed({ a: undefined }) await decoding.succeed({ a: null }, {}) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) await encoding.succeed({}) }) }) describe("asOption", () => { it("optionalKey -> Option", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.FiniteFromString).pipe( Schema.decodeTo( Schema.Option(Schema.Number), SchemaTransformation.transformOptional({ decode: Option.some, encode: Option.flatten }) ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.succeed({}, { a: Option.none() }) const encoding = asserts.encoding() await encoding.succeed({ a: Option.some(1) }, { a: "1" }) await encoding.succeed({ a: Option.none() }, {}) }) it("optional -> Option", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.FiniteFromString).pipe( Schema.decodeTo( Schema.Option(Schema.Number), SchemaTransformation.transformOptional({ decode: (on) => on.pipe(Option.filter((nu) => nu !== undefined), Option.some), encode: Option.flatten }) ) ) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1" }, { a: Option.some(1) }) await decoding.succeed({}, { a: Option.none() }) await decoding.succeed({ a: undefined }, { a: Option.none() }) const encoding = asserts.encoding() await encoding.succeed({ a: Option.some(1) }, { a: "1" }) await encoding.succeed({ a: Option.none() }, {}) }) }) it("decodeTo as composition", async () => { const From = Schema.Struct({ a: Schema.String, b: Schema.FiniteFromString }) const To = Schema.Struct({ a: Schema.FiniteFromString, b: Schema.UndefinedOr(Schema.Number) }) const schema = From.pipe(Schema.decodeTo(To)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1", b: "2" }, { a: 1, b: 2 }) const encoding = asserts.encoding() await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" }) await encoding.fail( { a: 1, b: NaN }, `Expected a finite number, got NaN at ["b"]` ) await encoding.fail( { a: 1, b: undefined }, `Expected number, got undefined at ["b"]` ) }) it("encodeTo as composition", async () => { const From = Schema.Struct({ a: Schema.String, b: Schema.FiniteFromString }) const To = Schema.Struct({ a: Schema.FiniteFromString, b: Schema.UndefinedOr(Schema.Number) }) const schema = To.pipe(Schema.encodeTo(From)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1", b: "2" }, { a: 1, b: 2 }) await decoding.fail( { a: "1", b: null }, `Expected string, got null at ["b"]` ) const encoding = asserts.encoding() await encoding.succeed({ a: 1, b: 2 }, { a: "1", b: "2" }) await encoding.fail( { a: 1, b: NaN }, `Expected a finite number, got NaN at ["b"]` ) await encoding.fail( { a: 1, b: undefined }, `Expected number, got undefined at ["b"]` ) }) describe("checkEffect", () => { it("no context", async () => { const schema = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.checkEffect((s) => Effect.gen(function*() { if (s.length === 0) { return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" }) } }).pipe(Effect.delay(100)) ), encode: SchemaGetter.passthrough() }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "", "input should not be empty string" ) }) it("with context", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.String.pipe( Schema.decode({ decode: SchemaGetter.checkEffect((s) => Effect.gen(function*() { yield* Service if (s.length === 0) { return new SchemaIssue.InvalidValue(Option.some(s), { message: "input should not be empty string" }) } }) ), encode: SchemaGetter.passthrough() }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide( Service, { fallback: Effect.succeed("b") } ) await decoding.succeed("a") await decoding.fail( "", "input should not be empty string" ) }) }) describe("is", () => { it("FiniteFromString", () => { const schema = Schema.FiniteFromString const is = Schema.is(schema) assertTrue(is(1)) assertFalse(is("a")) }) }) describe("asserts", () => { it("FiniteFromString", () => { const schema = Schema.FiniteFromString try { Schema.asserts(schema, 1) } catch { fail("Expected asserts to not throw an error") } try { Schema.asserts(schema, "a") fail("Expected asserts to throw an error") } catch (e) { ok(e instanceof Error) strictEqual(e.message, `Expected number, got "a"`) } }) }) describe("decodeUnknownPromise / encodeUnknownPromise", () => { it("FiniteFromString", async () => { const schema = Schema.FiniteFromString const decodeUnknownPromise = Schema.decodeUnknownPromise(schema) const encodeUnknownPromise = Schema.encodeUnknownPromise(schema) const decodeUnknownPromiseIssue = SchemaParser.decodeUnknownPromise(schema) const encodeUnknownPromiseIssue = SchemaParser.encodeUnknownPromise(schema) const r1 = await decodeUnknownPromise("1").then(Result.succeed, Result.fail) deepStrictEqual(r1, Result.succeed(1)) const r2 = await decodeUnknownPromise(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r2)) assertTrue(Schema.isSchemaError(r2.failure)) strictEqual(r2.failure.message, "Expected string, got null") const r3 = await encodeUnknownPromise(1).then(Result.succeed, Result.fail) deepStrictEqual(r3, Result.succeed("1")) const r4 = await encodeUnknownPromise(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r4)) assertTrue(Schema.isSchemaError(r4.failure)) strictEqual(r4.failure.message, "Expected number, got null") const r5 = await decodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r5)) assertTrue(r5.failure instanceof Error) strictEqual(r5.failure.message, "Expected string, got null") const r6 = await encodeUnknownPromiseIssue(null).then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r6)) assertTrue(r6.failure instanceof Error) strictEqual(r6.failure.message, "Expected number, got null") }) it("should reject with an error when the cause contains both a schema issue and a defect", async () => { const cause = Cause.combine( Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.failCause(cause)), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.failCause(cause)) })) const r1 = await Schema.decodeUnknownPromise(decodeSchema)("a").then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r1)) assertTrue(r1.failure instanceof Error) strictEqual(r1.failure.message, "Promise adapter can only reject schema errors") assertTrue(Cause.hasDies(r1.failure.cause as Cause.Cause)) const r2 = await Schema.encodeUnknownPromise(encodeSchema)("a").then(Result.succeed, Result.fail) assertTrue(Result.isFailure(r2)) assertTrue(r2.failure instanceof Error) strictEqual(r2.failure.message, "Promise adapter can only reject schema errors") assertTrue(Cause.hasDies(r2.failure.cause as Cause.Cause)) }) }) describe("decodeUnknownOption / encodeUnknownOption", () => { it("FiniteFromString", () => { const schema = Schema.FiniteFromString const decodeUnknownOption = Schema.decodeUnknownOption(schema) const encodeUnknownOption = Schema.encodeUnknownOption(schema) const r1 = decodeUnknownOption("1") assertTrue(Option.isSome(r1)) strictEqual(r1.value, 1) assertTrue(Option.isNone(decodeUnknownOption(null))) const r2 = encodeUnknownOption(1) assertTrue(Option.isSome(r2)) strictEqual(r2.value, "1") assertTrue(Option.isNone(encodeUnknownOption(null))) }) it("should throw an error when the cause is not a schema issue", () => { const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.die(new Error("decode defect"))), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.die(new Error("encode defect"))) })) throws(() => Schema.decodeUnknownOption(decodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) throws(() => Schema.encodeUnknownOption(encodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.failCause(cause)), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.failCause(cause)) })) throws(() => Schema.decodeUnknownOption(decodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) throws(() => Schema.encodeUnknownOption(encodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Option adapter can only return none for schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) }) describe("decodeUnknownResult / encodeUnknownResult", () => { it("FiniteFromString", () => { const schema = Schema.FiniteFromString const decodeUnknownResult = Schema.decodeUnknownResult(schema) const encodeUnknownResult = Schema.encodeUnknownResult(schema) const r1 = decodeUnknownResult("1") assertTrue(Result.isSuccess(r1)) strictEqual(r1.success, 1) const r2 = decodeUnknownResult(null) assertTrue(Result.isFailure(r2)) assertTrue(Schema.isSchemaError(r2.failure)) strictEqual(r2.failure.message, "Expected string, got null") const r3 = encodeUnknownResult(1) assertTrue(Result.isSuccess(r3)) strictEqual(r3.success, "1") const r4 = encodeUnknownResult(null) assertTrue(Result.isFailure(r4)) assertTrue(Schema.isSchemaError(r4.failure)) strictEqual(r4.failure.message, "Expected number, got null") const r5 = SchemaParser.decodeUnknownResult(schema)(null) assertTrue(Result.isFailure(r5)) assertTrue(SchemaIssue.isIssue(r5.failure)) strictEqual(r5.failure.toString(), "Expected string, got null") const r6 = SchemaParser.encodeUnknownResult(schema)(null) assertTrue(Result.isFailure(r6)) assertTrue(SchemaIssue.isIssue(r6.failure)) strictEqual(r6.failure.toString(), "Expected number, got null") }) it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.failCause(cause)), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.failCause(cause)) })) throws(() => Schema.decodeUnknownResult(decodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Result adapter can only return schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) throws(() => Schema.encodeUnknownResult(encodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Result adapter can only return schema issues") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) }) describe("decodeUnknownSync / encodeUnknownSync", () => { it("FiniteFromString", () => { const schema = Schema.FiniteFromString strictEqual(Schema.decodeUnknownSync(schema)("1"), 1) strictEqual(Schema.encodeUnknownSync(schema)(1), "1") throws(() => Schema.decodeUnknownSync(schema)(null), (e) => { assertTrue(Schema.isSchemaError(e)) strictEqual(e.message, "Expected string, got null") }) throws(() => Schema.encodeUnknownSync(schema)(null), (e) => { assertTrue(Schema.isSchemaError(e)) strictEqual(e.message, "Expected number, got null") }) throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => { assertTrue(e instanceof Error) assertTrue(SchemaIssue.isIssue(e.cause)) strictEqual(e.cause.toString(), "Expected string, got null") }) throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => { assertTrue(e instanceof Error) assertTrue(SchemaIssue.isIssue(e.cause)) strictEqual(e.cause.toString(), "Expected number, got null") }) }) it("should throw an error when the cause contains both a schema issue and a defect", () => { const cause = Cause.combine( Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.failCause(cause)), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.failCause(cause)) })) throws(() => Schema.decodeUnknownSync(decodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Sync adapter can only throw schema errors") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) throws(() => Schema.encodeUnknownSync(encodeSchema)("a"), (e) => { assertTrue(e instanceof Error) strictEqual(e.message, "Sync adapter can only throw schema errors") assertTrue(Cause.hasDies(e.cause as Cause.Cause)) }) }) }) describe("decodeUnknownResult", () => { it("should throw on async decoding", () => { const AsyncString = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter((os: Option.Option) => Effect.gen(function*() { yield* Effect.sleep("10 millis") return os }) ), encode: SchemaGetter.passthrough() })) const schema = AsyncString throws(() => SchemaParser.decodeUnknownResult(schema)("1")) }) it("should throw on missing dependency", () => { class MagicNumber extends Context.Service()("MagicNumber") {} const DepString = Schema.Number.pipe(Schema.decode({ decode: SchemaGetter.onSome((n) => Effect.gen(function*() { const magicNumber = yield* MagicNumber return Option.some(n * magicNumber) }) ), encode: SchemaGetter.passthrough() })) const schema = DepString throws(() => SchemaParser.decodeUnknownResult(schema as any)(1)) }) }) describe("decodeUnknownExit", () => { it("should die on async decoding", () => { const AsyncString = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter((os: Option.Option) => Effect.gen(function*() { yield* Effect.sleep("10 millis") return os }) ), encode: SchemaGetter.passthrough() })) const schema = AsyncString const exit = SchemaParser.decodeUnknownExit(schema)("1") assertTrue(Exit.hasDies(exit)) }) it("should die on missing dependency", () => { class MagicNumber extends Context.Service()("MagicNumber") {} const DepString = Schema.Number.pipe(Schema.decode({ decode: SchemaGetter.onSome((n) => Effect.gen(function*() { const magicNumber = yield* MagicNumber return Option.some(n * magicNumber) }) ), encode: SchemaGetter.passthrough() })) const schema = DepString const exit = SchemaParser.decodeUnknownExit(schema as any)(1) assertTrue(Exit.hasDies(exit)) }) }) describe("decodeUnknownExit / encodeUnknownExit", () => { it("should preserve mixed schema issue and defect causes", () => { const cause = Cause.combine( Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })), Cause.die(new Error("defect")) ) const decodeSchema = Schema.String.pipe(Schema.decode({ decode: new SchemaGetter.Getter(() => Effect.failCause(cause)), encode: SchemaGetter.passthrough() })) const encodeSchema = Schema.String.pipe(Schema.encode({ decode: SchemaGetter.passthrough(), encode: new SchemaGetter.Getter(() => Effect.failCause(cause)) })) const decodeExit = Schema.decodeUnknownExit(decodeSchema)("a") assertTrue(Exit.isFailure(decodeExit)) assertTrue(Exit.hasDies(decodeExit)) const decodeError = Cause.findError(decodeExit.cause) assertTrue(Result.isSuccess(decodeError)) assertTrue(Schema.isSchemaError(decodeError.success)) const encodeExit = Schema.encodeUnknownExit(encodeSchema)("a") assertTrue(Exit.isFailure(encodeExit)) assertTrue(Exit.hasDies(encodeExit)) const encodeError = Cause.findError(encodeExit.cause) assertTrue(Result.isSuccess(encodeError)) assertTrue(Schema.isSchemaError(encodeError.success)) }) }) describe("annotateKey", () => { describe("the messageMissingKey annotation should be used as a error message", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.String.pipe(Schema.annotateKey({ messageMissingKey: "this field is required" })) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( {}, `this field is required at ["a"]` ) }) it("Tuple", async () => { const schema = Schema.Tuple([ Schema.String.pipe(Schema.annotateKey({ messageMissingKey: "this element is required" })) ]) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( [], `this element is required at [0]` ) }) }) }) describe("annotateEncoded", () => { it("non-transforming schema", () => { const schema = Schema.String.pipe( Schema.annotateEncoded({ title: "encoded title" }) ) strictEqual(SchemaAST.toEncoded(schema.ast).annotations?.title, "encoded title") }) it("transforming schema", () => { const schema = Schema.NumberFromString.pipe( Schema.annotateEncoded({ title: "encoded title" }) ) strictEqual(SchemaAST.toEncoded(schema.ast).annotations?.title, "encoded title") }) }) describe("Struct.mapFields", () => { it("evolve", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.evolve({ a: (v) => Schema.optionalKey(v) })) equals(schema.fields, { a: Schema.optionalKey(Schema.String), b: Schema.Number }) }) it("evolveKeys", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.evolveKeys({ a: (k) => Str.toUpperCase(k) })) equals(schema.fields, { A: Schema.String, b: Schema.Number }) }) it("renameKeys", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number, c: Schema.Boolean }).mapFields(Struct.renameKeys({ a: "A", b: "B" })) equals(schema.fields, { A: Schema.String, B: Schema.Number, c: Schema.Boolean }) }) it("evolveEntries", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.evolveEntries({ a: (k, v) => [Str.toUpperCase(k), Schema.optionalKey(v)] })) equals(schema.fields, { A: Schema.optionalKey(Schema.String), b: Schema.Number }) }) it("optionalKey", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.optionalKey)) equals(schema.fields, { a: Schema.optionalKey(Schema.String), b: Schema.optionalKey(Schema.Number) }) }) it("mapPick", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.mapPick(["a"], Schema.optionalKey)) equals(schema.fields, { a: Schema.optionalKey(Schema.String), b: Schema.Number }) }) it("mapOmit", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.mapOmit(["b"], Schema.optionalKey)) equals(schema.fields, { a: Schema.optionalKey(Schema.String), b: Schema.Number }) }) it("optional", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.optional)) equals(schema.fields, { a: Schema.optional(Schema.String), b: Schema.optional(Schema.Number) }) }) it("mutableKey", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.mutableKey)) equals(schema.fields, { a: Schema.mutableKey(Schema.String), b: Schema.mutableKey(Schema.Number) }) }) it("readonlyKey", () => { const schema = Schema.Struct({ a: Schema.mutableKey(Schema.String), b: Schema.mutableKey(Schema.Number) }).mapFields(Struct.map(Schema.readonlyKey)) equals(schema.fields, { a: Schema.String, b: Schema.Number }) }) it("mutable", () => { const schema = Schema.Struct({ a: Schema.Array(Schema.String), b: Schema.Tuple([Schema.Number]) }).mapFields(Struct.map(Schema.mutable)) equals(schema.fields, { a: Schema.mutable(Schema.Array(Schema.String)), b: Schema.mutable(Schema.Tuple([Schema.Number])) }) }) it("NullOr", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.NullOr)) equals(schema.fields, { a: Schema.NullOr(Schema.String), b: Schema.NullOr(Schema.Number) }) }) it("UndefinedOr", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.UndefinedOr)) equals(schema.fields, { a: Schema.UndefinedOr(Schema.String), b: Schema.UndefinedOr(Schema.Number) }) }) it("NullishOr", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).mapFields(Struct.map(Schema.NullishOr)) equals(schema.fields, { a: Schema.NullishOr(Schema.String), b: Schema.NullishOr(Schema.Number) }) }) it("should work with flow", () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.FiniteFromString, c: Schema.Boolean }).mapFields(flow( Struct.map(Schema.NullOr), Struct.mapPick(["a", "c"], Schema.mutableKey) )) equals(schema.fields, { a: Schema.mutableKey(Schema.NullOr(Schema.String)), b: Schema.NullOr(Schema.FiniteFromString), c: Schema.mutableKey(Schema.NullOr(Schema.Boolean)) }) }) }) describe("Tuple.mapElements", () => { it("appendElement", () => { const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.appendElement(Schema.Number)) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Number]) }) it("appendElements", () => { const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.appendElements([Schema.Number, Schema.Boolean])) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Number, Schema.Boolean]) }) it("pick", () => { const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.pick([0, 2])) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Boolean]) }) it("omit", () => { const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements(Tuple.omit([1])) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.Boolean]) }) describe("evolve", () => { it("readonly [string] -> readonly [string?]", () => { const schema = Schema.Tuple([Schema.String]).mapElements(Tuple.evolve([(v) => Schema.optionalKey(v)])) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.optionalKey(Schema.String)]) }) it("readonly [string, number] -> readonly [string, number?]", () => { const schema = Schema.Tuple([Schema.String, Schema.Number]).mapElements( Tuple.evolve([undefined, (v) => Schema.optionalKey(v)]) ) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.String, Schema.optionalKey(Schema.Number)]) }) }) describe("renameIndices", () => { it("partial index mapping", () => { const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( Tuple.renameIndices(["1", "0"]) ) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.Number, Schema.String, Schema.Boolean]) }) it("full index mapping", () => { const schema = Schema.Tuple([Schema.String, Schema.Number, Schema.Boolean]).mapElements( Tuple.renameIndices(["2", "1", "0"]) ) TestSchema.Asserts.ast.elements.equals(schema.elements, [Schema.Boolean, Schema.Number, Schema.String]) }) }) it("NullOr", () => { const schema = Schema.Tuple([Schema.String, Schema.Number]).mapElements(Tuple.map(Schema.NullOr)) TestSchema.Asserts.ast.elements.equals(schema.elements, [ Schema.NullOr(Schema.String), Schema.NullOr(Schema.Number) ]) }) }) describe("Union.mapMembers", () => { it("appendElement", () => { const schema = Schema.Union([Schema.String, Schema.Number]).mapMembers(Tuple.appendElement(Schema.Boolean)) TestSchema.Asserts.ast.elements.equals(schema.members, [Schema.String, Schema.Number, Schema.Boolean]) }) it("evolve", () => { const schema = Schema.Union([Schema.String, Schema.Number, Schema.Boolean]).mapMembers( Tuple.evolve([ (v) => Schema.Array(v), undefined, (v) => Schema.Array(v) ]) ) TestSchema.Asserts.ast.elements.equals(schema.members, [ Schema.Array(Schema.String), Schema.Number, Schema.Array(Schema.Boolean) ]) }) it("Array", () => { const schema = Schema.Union([Schema.String, Schema.Number]).mapMembers(Tuple.map(Schema.Array)) TestSchema.Asserts.ast.elements.equals(schema.members, [ Schema.Array(Schema.String), Schema.Array(Schema.Number) ]) }) }) describe("Literals.mapMembers", () => { it("evolve", () => { const schema = Schema.Literals(["a", "b", "c"]).mapMembers(Tuple.evolve([ (a) => Schema.Struct({ _tag: a, a: Schema.String }), (b) => Schema.Struct({ _tag: b, b: Schema.Number }), (c) => Schema.Struct({ _tag: c, c: Schema.Boolean }) ])) TestSchema.Asserts.ast.elements.equals(schema.members, [ Schema.Struct({ _tag: Schema.Literal("a"), a: Schema.String }), Schema.Struct({ _tag: Schema.Literal("b"), b: Schema.Number }), Schema.Struct({ _tag: Schema.Literal("c"), c: Schema.Boolean }) ]) }) }) describe("encodeKeys", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString, b: Schema.FiniteFromString }).pipe(Schema.encodeKeys({ a: "c" })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ c: "1", b: "2" }, { a: 1, b: 2 }) const encoding = asserts.encoding() await encoding.succeed({ a: 1, b: 2 }, { c: "1", b: "2" }) }) it("Class", async () => { class A extends Schema.Class("A")({ a: Schema.FiniteFromString, b: Schema.String }) {} const schema = A.pipe(Schema.encodeKeys({ a: "c" })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ c: "1", b: "b" }, new A({ a: 1, b: "b" })) const encoding = asserts.encoding() await encoding.succeed(new A({ a: 1, b: "b" }), { c: "1", b: "b" }) }) it("supports symbol source keys", () => { const field = Symbol("field") const schema = Schema.Struct({ [field]: Schema.String }).pipe(Schema.encodeKeys({ [field]: "field" })) deepStrictEqual(Schema.decodeUnknownSync(schema)({ field: "a" }), { [field]: "a" }) deepStrictEqual(Schema.encodeSync(schema)({ [field]: "a" }), { field: "a" }) }) it("supports symbol destination keys", () => { const field = Symbol("field") const schema = Schema.Struct({ field: Schema.String }).pipe(Schema.encodeKeys({ field })) deepStrictEqual(Schema.decodeUnknownSync(schema)({ [field]: "a" }), { field: "a" }) deepStrictEqual(Schema.encodeSync(schema)({ field: "a" }), { [field]: "a" }) }) it("rejects duplicate destination keys", () => { throws( () => Schema.Struct({ a: Schema.String, b: Schema.String }).pipe(Schema.encodeKeys({ a: "c", b: "c" })), (e) => { assertInclude(String(e), "Duplicate encoded keys") } ) }) it("rejects destination keys that collide with unmapped fields", () => { throws( () => Schema.Struct({ a: Schema.String, b: Schema.String }).pipe(Schema.encodeKeys({ a: "b" })), (e) => { assertInclude(String(e), "Duplicate encoded keys") } ) }) it("rejects canonical number and string destination key collisions", () => { throws( () => Schema.Struct({ a: Schema.String, b: Schema.String }).pipe(Schema.encodeKeys({ a: 1, b: "1" })), (e) => { assertInclude(String(e), "Duplicate encoded keys") } ) }) }) describe("Schema.makeFilter", () => { it("returns undefined", async () => { const schema = Schema.String.check(Schema.makeFilter(() => undefined)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") }) it("returns true", async () => { const schema = Schema.String.check(Schema.makeFilter(() => true)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") }) it("returns false", async () => { const schema = Schema.String.check(Schema.makeFilter(() => false)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `Expected , got "a"` ) }) it("returns string", async () => { const schema = Schema.String.check(Schema.makeFilter(() => "error message")) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `error message` ) }) describe("returns issue", () => { it("abort: false", async () => { const schema = Schema.String.check( Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), { title: "filter title 1" }), Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `error message 1 error message 2` ) }) it("abort: true", async () => { const schema = Schema.String.check( Schema.makeFilter((s) => new SchemaIssue.InvalidValue(Option.some(s), { message: "error message 1" }), { title: "filter title 1" }, true), Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `error message 1` ) }) }) describe("returns object", () => { it("abort: false", async () => { const schema = Schema.String.check( Schema.makeFilter(() => ({ path: ["a"], issue: "error message 1" }), { title: "filter title 1" }), Schema.makeFilter(() => false, { title: "filter title 2", message: "error message 2" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `error message 1 at ["a"] error message 2` ) }) it("abort: true", async () => { const schema = Schema.String.check( Schema.makeFilter(() => ({ path: ["a"], issue: "error message 1" }), { title: "error title 1" }, true), Schema.makeFilter(() => false, { title: "error title 2", message: "error message 2" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `error message 1 at ["a"]` ) }) it("issue: Issue", async () => { const schema = Schema.String.check( Schema.makeFilter( (s) => ({ path: ["a"], issue: new SchemaIssue.InvalidValue(Option.some(s), { message: "custom issue" }) }), { title: "filter title" } ) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `custom issue at ["a"]` ) }) }) describe("returns array", () => { it("empty array is treated as success", async () => { const schema = Schema.String.check(Schema.makeFilter(() => [])) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") }) it("single-element array collapses to the element", async () => { const schema = Schema.String.check( Schema.makeFilter(() => [{ path: ["a"], issue: "error message 1" }], { title: "filter title" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.fail( "a", `error message 1 at ["a"]` ) }) it("multi-element array groups into a Composite", async () => { const schema = Schema.String.check( Schema.makeFilter(() => [ { path: ["a"], issue: "error message 1" }, { path: ["b"], issue: "error message 2" } ], { title: "filter title" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `error message 1 at ["a"] error message 2 at ["b"]` ) }) it("array mixing string, Issue, and { path, issue }", async () => { const schema = Schema.String.check( Schema.makeFilter((s) => [ "top-level message", new SchemaIssue.InvalidValue(Option.some(s), { message: "direct issue" }), { path: ["a"], issue: "pointed message" } ], { title: "filter title" }) ) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding({ parseOptions: { errors: "all" } }) await decoding.fail( "a", `top-level message direct issue pointed message at ["a"]` ) }) }) }) describe("extendTo", () => { it("Struct", async () => { const schema = Schema.Struct({ a: Schema.String, b: Schema.Number }).pipe(Schema.extendTo({ c: Schema.String }, { c: (value) => Option.some(value.a + "c" + value.b) })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "1", b: 2 }, { a: "1", b: 2, c: "1c2" }) const encoding = asserts.encoding() await encoding.succeed({ a: "1", b: 2, c: "1c2" }, { a: "1", b: 2 }) }) it("Union", async () => { const Circle = Schema.Struct({ radius: Schema.Number }) const Square = Schema.Struct({ sideLength: Schema.Number }) const DiscriminatedShape = Schema.Union([ Circle.pipe(Schema.extendTo({ kind: Schema.tag("circle") }, { kind: () => Option.some("circle" as const) })), Square.pipe(Schema.extendTo({ kind: Schema.tag("square") }, { kind: () => Option.some("square" as const) })) ]) const asserts = new TestSchema.Asserts(DiscriminatedShape) const decoding = asserts.decoding() await decoding.succeed({ radius: 1 }, { radius: 1, kind: "circle" }) await decoding.succeed({ sideLength: 1 }, { sideLength: 1, kind: "square" }) const encoding = asserts.encoding() await encoding.succeed({ radius: 1, kind: "circle" }, { radius: 1 }) await encoding.succeed({ sideLength: 1, kind: "square" }, { sideLength: 1 }) }) }) describe("Tagged unions", () => { describe("toTaggedUnion", () => { it("should augment a union of structs", () => { const b = Symbol.for("B") const schema = Schema.Union([ Schema.Struct({ _tag: Schema.Literal("A"), a: Schema.String }), Schema.Struct({ _tag: Schema.UniqueSymbol(b), b: Schema.FiniteFromString }), Schema.Union([ Schema.Struct({ _tag: Schema.Literal(1), c: Schema.Boolean }), Schema.Struct({ _tag: Schema.Literal("D"), d: Schema.Date }) ]) ]).pipe(Schema.toTaggedUnion("_tag")) // cases deepStrictEqual(schema.cases.A, schema.members[0]) deepStrictEqual(schema.cases[b], schema.members[1]) deepStrictEqual(schema.cases[1], schema.members[2].members[0]) deepStrictEqual(schema.cases["1"], schema.members[2].members[0]) deepStrictEqual(schema.cases.D, schema.members[2].members[1]) // isAnyOf const isAOr1 = schema.isAnyOf(["A", 1]) assertTrue(isAOr1({ _tag: "A", a: "a" })) assertTrue(isAOr1({ _tag: 1, c: true })) assertFalse(isAOr1({ _tag: "D", d: new Date() })) assertFalse(isAOr1({ _tag: b, b: 1 })) // guards assertTrue(schema.guards.A({ _tag: "A", a: "a" })) assertFalse(schema.guards.A({ _tag: "A", a: 1 })) assertTrue(schema.guards[b]({ _tag: b, b: 1 })) assertFalse(schema.guards[b]({ _tag: b, b: "b" })) assertTrue(schema.guards[1]({ _tag: 1, c: true })) assertFalse(schema.guards[1]({ _tag: 1, c: 1 })) assertTrue(schema.guards.D({ _tag: "D", d: new Date() })) assertFalse(schema.guards.D({ _tag: "D", d: "d" })) // match deepStrictEqual( schema.match({ _tag: "A", a: "a" }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }), "A" ) deepStrictEqual( pipe({ _tag: "A", a: "a" }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })), "A" ) deepStrictEqual( schema.match({ _tag: b, b: 1 }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }), "B" ) deepStrictEqual( pipe({ _tag: b, b: 1 }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })), "B" ) deepStrictEqual( schema.match({ _tag: 1, c: true }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }), "C" ) deepStrictEqual( pipe({ _tag: 1, c: true }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" })), "C" ) deepStrictEqual( schema.match({ _tag: "D", d: new Date() }, { A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }), "D" ) deepStrictEqual( pipe( { _tag: "D", d: new Date() }, schema.match({ A: () => "A", [b]: () => "B", 1: () => "C", D: () => "D" }) ), "D" ) }) it("should support multiple tags", () => { const schema = Schema.Union([ Schema.Struct({ _tag: Schema.tag("A"), type: Schema.tag("TypeA"), a: Schema.String }), Schema.Struct({ _tag: Schema.tag("B"), type: Schema.tag("TypeB"), b: Schema.FiniteFromString }) ]).pipe(Schema.toTaggedUnion("type")) // cases deepStrictEqual(schema.cases.TypeA, schema.members[0]) deepStrictEqual(schema.cases.TypeB, schema.members[1]) }) it("should augment a union of classes", () => { class A extends Schema.Class("A")({ _tag: Schema.tag("A"), a: Schema.String }) {} class B extends Schema.Class("B")({ _tag: Schema.tag("B"), b: Schema.FiniteFromString }) {} const schema = Schema.Union([A, B]).pipe(Schema.toTaggedUnion("_tag")) // cases deepStrictEqual(schema.cases.A, A) deepStrictEqual(schema.cases.B, B) }) }) describe("TaggedUnion", () => { it("should create a tagged union", () => { const schema = Schema.TaggedUnion({ A: { a: Schema.String }, C: { c: Schema.Boolean }, B: { b: Schema.FiniteFromString } }).annotate({}) const { A, B, C } = schema.cases // cases strictEqual(A.fields._tag.ast.literal, "A") strictEqual(A.fields.a, Schema.String) strictEqual(B.fields._tag.ast.literal, "B") strictEqual(B.fields.b, Schema.FiniteFromString) strictEqual(C.fields._tag.ast.literal, "C") strictEqual(C.fields.c, Schema.Boolean) // isAnyOf const isAOrB = schema.isAnyOf(["A", "B"]) assertTrue(isAOrB({ _tag: "A", a: "a" })) assertTrue(isAOrB({ _tag: "B", b: 1 })) assertFalse(isAOrB({ _tag: "C", c: true })) // guards assertTrue(schema.guards.A({ _tag: "A", a: "a" })) assertTrue(schema.guards.B({ _tag: "B", b: 1 })) assertTrue(schema.guards.C({ _tag: "C", c: true })) assertFalse(schema.guards.A({ _tag: "A", b: 1 })) assertFalse(schema.guards.B({ _tag: "B", a: "a" })) assertFalse(schema.guards.C({ _tag: "C", c: 1 })) // match deepStrictEqual( schema.match({ _tag: "A", a: "a" }, { A: () => "A", B: () => "B", C: () => "C" }), "A" ) deepStrictEqual( pipe({ _tag: "A", a: "a" }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })), "A" ) deepStrictEqual( schema.match({ _tag: "B", b: 1 }, { A: () => "A", B: () => "B", C: () => "C" }), "B" ) deepStrictEqual( pipe({ _tag: "B", b: 1 }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })), "B" ) deepStrictEqual( schema.match({ _tag: "C", c: true }, { A: () => "A", B: () => "B", C: () => "C" }), "C" ) deepStrictEqual( pipe({ _tag: "C", c: true }, schema.match({ A: () => "A", B: () => "B", C: () => "C" })), "C" ) }) }) }) describe("withDecodingDefaultKey", () => { it("should return a decoding default value if the key is missing", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1"))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: 1 }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( { a: undefined }, `Expected string, got undefined at ["a"]` ) }) it("by default should pass through the value", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1"))) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) }) it("should omit the value if the encoding strategy is set to omit", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe( Schema.withDecodingDefaultKey(Effect.succeed("1"), { encodingStrategy: "omit" }) ) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, {}) }) it("nested default values", async () => { const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.succeed("1"))) }).pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: { b: 1 } }) await decoding.succeed({ a: {} }, { a: { b: 1 } }) await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) await decoding.fail( { a: { b: undefined } }, `Expected string, got undefined at ["a"]["b"]` ) }) it("Effect failing with SchemaError propagates as decode failure", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey( Effect.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) ) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( {}, `decoding default failed at ["a"]` ) }) it("Effect failing with SchemaError and a defect preserves the mixed cause", () => { const cause = Cause.combine( Cause.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) ), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey(Effect.failCause(cause))) }) const exit = Schema.decodeUnknownExit(schema)({}) assertTrue(Exit.isFailure(exit)) assertTrue(Exit.hasDies(exit)) const error = Cause.findError(exit.cause) assertTrue(Result.isSuccess(error)) assertTrue(Schema.isSchemaError(error.success)) strictEqual( error.success.message, `decoding default failed at ["a"]` ) }) it("default Effect can require a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultKey( Effect.gen(function*() { const service = yield* Service return yield* service.fallback }) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed("3") }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.succeed({}, { a: 3 }) }) }) describe("withDecodingDefault", () => { it("should return a decoding default value if the key is missing", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: 1 }) await decoding.succeed({ a: undefined }, { a: 1 }) await decoding.succeed({ a: "2" }, { a: 2 }) }) it("should return a decoding default value if the schema is used as standalone and the input is undefined", async () => { const schema = Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed("a"))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(undefined, "a") await decoding.succeed("b", "b") }) it("by default should pass through the value", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"))) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) }) it("should omit the value if the encoding strategy is set to omit", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"), { encodingStrategy: "omit" })) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, {}) }) it("nested default values", async () => { const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.succeed("1"))) }).pipe(Schema.withDecodingDefault(Effect.succeed({}))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: { b: 1 } }) await decoding.succeed({ a: {} }, { a: { b: 1 } }) await decoding.succeed({ a: undefined }, { a: { b: 1 } }) await decoding.succeed({ a: { b: undefined } }, { a: { b: 1 } }) await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) }) it("Effect failing with SchemaError and a defect preserves the mixed cause", () => { const cause = Cause.combine( Cause.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) ), Cause.die(new Error("defect")) ) const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault(Effect.failCause(cause))) }) const exit = Schema.decodeUnknownExit(schema)({}) assertTrue(Exit.isFailure(exit)) assertTrue(Exit.hasDies(exit)) const error = Cause.findError(exit.cause) assertTrue(Result.isSuccess(error)) assertTrue(Schema.isSchemaError(error.success)) strictEqual( error.success.message, `decoding default failed at ["a"]` ) }) it("default Effect can require a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefault( Effect.gen(function*() { const service = yield* Service return yield* service.fallback }) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed("3") }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.succeed({}, { a: 3 }) await decoding.succeed({ a: undefined }, { a: 3 }) }) }) describe("withDecodingDefaultTypeKey", () => { it("should return a decoding default value if the key is missing", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: 1 }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( { a: undefined }, `Expected string, got undefined at ["a"]` ) }) it("by default should pass through the value", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1))) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) }) it("should omit the value if the encoding strategy is set to omit", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe( Schema.withDecodingDefaultTypeKey(Effect.succeed(1), { encodingStrategy: "omit" }) ) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, {}) }) it("nested default values", async () => { const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed(1))) }).pipe(Schema.withDecodingDefaultTypeKey(Effect.succeed({ b: 1 }))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: { b: 1 } }) await decoding.succeed({ a: {} }, { a: { b: 1 } }) await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) await decoding.fail( { a: { b: undefined } }, `Expected string, got undefined at ["a"]["b"]` ) }) it("Effect failing with SchemaError propagates as decode failure", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey( Effect.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) ) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( {}, `decoding default failed at ["a"]` ) }) it("default Effect can require a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultTypeKey( Effect.gen(function*() { const service = yield* Service return yield* service.fallback }) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed(3) }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.succeed({}, { a: 3 }) }) }) describe("withDecodingDefaultType", () => { it("should return a decoding default value if the key is missing", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: 1 }) await decoding.succeed({ a: undefined }, { a: 1 }) await decoding.succeed({ a: "2" }, { a: 2 }) }) it("should return a decoding default value if the schema is used as standalone and the input is undefined", async () => { const schema = Schema.String.pipe(Schema.withDecodingDefaultType(Effect.succeed("a"))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(undefined, "a") await decoding.succeed("b", "b") }) it("by default should pass through the value", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1))) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, { a: "1" }) }) it("should omit the value if the encoding strategy is set to omit", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe( Schema.withDecodingDefaultType(Effect.succeed(1), { encodingStrategy: "omit" }) ) }) const asserts = new TestSchema.Asserts(schema) const encoding = asserts.encoding() await encoding.succeed({ a: 1 }, {}) }) it("nested default values", async () => { const schema = Schema.Struct({ a: Schema.Struct({ b: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType(Effect.succeed(1))) }).pipe(Schema.withDecodingDefaultType(Effect.succeed({ b: 1 }))) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({}, { a: { b: 1 } }) await decoding.succeed({ a: {} }, { a: { b: 1 } }) await decoding.succeed({ a: undefined }, { a: { b: 1 } }) await decoding.succeed({ a: { b: undefined } }, { a: { b: 1 } }) await decoding.succeed({ a: { b: "2" } }, { a: { b: 2 } }) }) it("Effect failing with SchemaError propagates as decode failure", async () => { const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType( Effect.fail( new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.none(), { message: "decoding default failed" })) ) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.fail( {}, `decoding default failed at ["a"]` ) }) it("default Effect can require a service", async () => { class Service extends Context.Service }>()("Service") {} const schema = Schema.Struct({ a: Schema.FiniteFromString.pipe(Schema.withDecodingDefaultType( Effect.gen(function*() { const service = yield* Service return yield* service.fallback }) )) }) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding().provide(Service, { fallback: Effect.succeed(3) }) await decoding.succeed({ a: "2" }, { a: 2 }) await decoding.succeed({}, { a: 3 }) await decoding.succeed({ a: undefined }, { a: 3 }) }) }) it("NonEmptyString", async () => { const schema = Schema.NonEmptyString const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "", `Expected a value with a length of at least 1, got ""` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "", `Expected a value with a length of at least 1, got ""` ) }) it("Char", async () => { const schema = Schema.Char const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail( "ab", `Expected a value with a length of 1, got "ab"` ) const encoding = asserts.encoding() await encoding.succeed("a") await encoding.fail( "ab", `Expected a value with a length of 1, got "ab"` ) }) it("Int", async () => { const schema = Schema.Int const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail( 1.1, `Expected an integer, got 1.1` ) await decoding.fail( NaN, `Expected an integer, got NaN` ) await decoding.fail( Infinity, `Expected an integer, got Infinity` ) await decoding.fail( -Infinity, `Expected an integer, got -Infinity` ) const encoding = asserts.encoding() await encoding.succeed(1) await encoding.fail( 1.1, `Expected an integer, got 1.1` ) }) it("Capitalize", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String.check(Schema.isCapitalized()), SchemaTransformation.capitalize() ) ) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("abc", "Abc") const encoding = asserts.encoding() await encoding.succeed("Abc") await encoding.fail( "abc", `Expected a string with the first character in uppercase, got "abc"` ) }) it("Uncapitalize", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String.check(Schema.isUncapitalized()), SchemaTransformation.uncapitalize() ) ) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("Abc", "abc") const encoding = asserts.encoding() await encoding.succeed("abc") await encoding.fail( "Abc", `Expected a string with the first character in lowercase, got "Abc"` ) }) it("Lowercase", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String.check(Schema.isLowercased()), SchemaTransformation.toLowerCase() ) ) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("ABC", "abc") const encoding = asserts.encoding() await encoding.succeed("abc") await encoding.fail( "ABC", `Expected a string with all characters in lowercase, got "ABC"` ) }) it("Uppercase", async () => { const schema = Schema.String.pipe( Schema.decodeTo( Schema.String.check(Schema.isUppercased()), SchemaTransformation.toUpperCase() ) ) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } const decoding = asserts.decoding() await decoding.succeed("abc", "ABC") const encoding = asserts.encoding() await encoding.succeed("ABC") await encoding.fail( "abc", `Expected a string with all characters in uppercase, got "abc"` ) }) }) describe("Getter", () => { it("succeed", async () => { const schema = Schema.Literal(0).pipe(Schema.decodeTo(Schema.Literal("a"), { decode: SchemaGetter.succeed("a"), encode: SchemaGetter.succeed(0) })) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(0, "a") await decoding.fail(1, `Expected 0, got 1`) const encoding = asserts.encoding() await encoding.succeed("a", 0) await encoding.fail("b", `Expected "a", got "b"`) }) }) describe("Check", () => { it("isStringFinite", async () => { const schema = Schema.String.check(Schema.isStringFinite()) deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isStringFinite", regExp: /^[+-]?\d*\.?\d+(?:[Ee][+-]?\d+)?$/ }) }) it("isStringBigInt", async () => { const schema = Schema.String.check(Schema.isStringBigInt()) deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isStringBigInt", regExp: /^-?\d+$/ }) }) it("isStringSymbol", async () => { const schema = Schema.String.check(Schema.isStringSymbol()) deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isStringSymbol", regExp: /^Symbol\((.*)\)$/ }) }) it("isUUID", async () => { const schema = Schema.String.check(Schema.isUUID()) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isUUID", regExp: /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|[fF]{8}-[fF]{4}-[fF]{4}-[fF]{4}-[fF]{12})$/, version: undefined }) const decoding = asserts.decoding() await decoding.succeed("00000000-0000-0000-0000-000000000000") await decoding.succeed("ffffffff-ffff-ffff-ffff-ffffffffffff") await decoding.succeed("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") await decoding.succeed("00000000-0000-4000-8000-000000000001") await decoding.fail( "00000000-0000-0000-0000-000000000001", `Expected a UUID, got "00000000-0000-0000-0000-000000000001"` ) }) it("isUUID version-specific checks reject nil and max UUIDs", async () => { const schema = Schema.String.check(Schema.isUUID(4)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("00000000-0000-4000-8000-000000000001") await decoding.fail( "00000000-0000-0000-0000-000000000000", `Expected a UUID v4, got "00000000-0000-0000-0000-000000000000"` ) await decoding.fail( "ffffffff-ffff-ffff-ffff-ffffffffffff", `Expected a UUID v4, got "ffffffff-ffff-ffff-ffff-ffffffffffff"` ) }) it("isGUID", async () => { const schema = Schema.String.check(Schema.isGUID()) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isGUID", regExp: /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/ }) const decoding = asserts.decoding() await decoding.succeed("00000000-0000-0000-0000-000000000001") await decoding.succeed("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") await decoding.fail( "not-a-guid", `Expected a GUID, got "not-a-guid"` ) }) it("isULID", async () => { const schema = Schema.String.check(Schema.isULID()) const asserts = new TestSchema.Asserts(schema) if (verifyGeneration) { asserts.arbitrary().verifyGeneration() } deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isULID", regExp: /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/ }) const decoding = asserts.decoding() await decoding.succeed("01H4PGGGJVN2DKP2K1H7EH996V") await decoding.fail( "", `Expected a string matching the RegExp ^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$, got ""` ) }) it("isBase64", async () => { const schema = Schema.String.check(Schema.isBase64()) deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isBase64", regExp: /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/ }) }) it("isBase64Url", async () => { const schema = Schema.String.check(Schema.isBase64Url()) deepStrictEqual(Schema.resolveAnnotations(schema)?.["meta"], { _tag: "isBase64Url", regExp: /^([0-9a-zA-Z-_]{4})*(([0-9a-zA-Z-_]{2}(==)?)|([0-9a-zA-Z-_]{3}(=)?))?$/ }) }) describe("brand", () => { it("single brand", async () => { const schema = Schema.String.pipe(Schema.brand("Positive")) deepStrictEqual(schema.ast.annotations?.brands, ["Positive"]) }) it("double brand", async () => { const schema = Schema.String.pipe(Schema.brand("Positive"), Schema.brand("Int")) deepStrictEqual(schema.ast.annotations?.brands, ["Positive", "Int"]) }) it("override the default identifier", async () => { const schema = Schema.String.pipe(Schema.brand("Positive"), Schema.brand("Int")).annotate({ identifier: "MyInt" }) deepStrictEqual(schema.ast.annotations?.brands, ["Positive", "Int"]) }) }) describe("fromBrand", () => { it("nominal", async () => { const schema = Schema.String.pipe(Schema.fromBrand("a", Brand.nominal>())) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed("a") await decoding.fail(1, `Expected string, got 1`) deepStrictEqual(schema.ast.annotations?.brands, ["a"]) }) it("single brand", async () => { type Int = number & Brand.Brand<"Int"> const Int = Brand.check(Schema.isInt()) const schema = Schema.Number.pipe(Schema.fromBrand("Int", Int)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail("a", `Expected number, got "a"`) await decoding.fail(1.2, `Expected an integer, got 1.2`) deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["Int"]) }) it("multiple brands", async () => { type Int = number & Brand.Brand<"Int"> const Int = Brand.check(Schema.isInt()) type Positive = number & Brand.Brand<"Positive"> const Positive = Brand.check(Schema.isGreaterThan(0)) const PositiveInt = Brand.all(Int, Positive) const schema = Schema.Number.pipe(Schema.fromBrand("PositiveInt", PositiveInt)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed(1) await decoding.fail("a", `Expected number, got "a"`) await decoding.fail(1.2, `Expected an integer, got 1.2`) await decoding.fail(-1, `Expected a value greater than 0, got -1`) deepStrictEqual(schema.ast.checks?.at(-1)?.annotations?.brands, ["PositiveInt"]) }) }) describe("requiredKey", () => { it("should make all optionalKey keys required", async () => { const schema = Schema.Struct({ a: Schema.optionalKey(Schema.String), b: Schema.optionalKey(Schema.Number) }).mapFields(Struct.map(Schema.requiredKey)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 1 }) await decoding.fail( { a: "a" }, `Missing key at ["b"]` ) await decoding.fail( { a: "a", b: undefined }, `Expected number, got undefined at ["b"]` ) await decoding.fail( { b: 1 }, `Missing key at ["a"]` ) }) it("should make all optional keys required", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.String), b: Schema.optional(Schema.Number) }).mapFields(Struct.map(Schema.requiredKey)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 1 }) await decoding.succeed({ a: "a", b: undefined }) await decoding.succeed({ a: undefined, b: 1 }) await decoding.succeed({ a: undefined, b: undefined }) await decoding.fail( { a: "a" }, `Missing key at ["b"]` ) await decoding.fail( { b: 1 }, `Missing key at ["a"]` ) }) }) describe("required", () => { it("should make all optional keys required", async () => { const schema = Schema.Struct({ a: Schema.optional(Schema.String), b: Schema.optional(Schema.Number) }).mapFields(Struct.map(Schema.required)) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", b: 1 }) await decoding.fail( { a: "a" }, `Missing key at ["b"]` ) await decoding.fail( { a: "a", b: undefined }, `Expected number, got undefined at ["b"]` ) await decoding.fail( { b: 1 }, `Missing key at ["a"]` ) await decoding.fail( { a: undefined, b: 1 }, `Expected string, got undefined at ["a"]` ) }) }) it("fieldsAssign", async () => { const schema = Schema.Union([ Schema.Struct({ a: Schema.String }), Schema.Struct({ b: Schema.Number }) ]).mapMembers(Tuple.map(Schema.fieldsAssign({ c: Schema.Number }))) const asserts = new TestSchema.Asserts(schema) const decoding = asserts.decoding() await decoding.succeed({ a: "a", c: 1 }) await decoding.succeed({ b: 1, c: 1 }) await decoding.fail( { a: "a" }, `Missing key at ["c"] Missing key at ["b"]` ) await decoding.fail( { b: 1 }, `Missing key at ["a"] Missing key at ["c"]` ) }) describe("asClass", () => { it("wrapping a primitive schema", () => { class A extends Schema.asClass(Schema.String) {} strictEqual(Schema.decodeUnknownSync(A)("a"), "a") }) it("static getter using this", () => { class A extends Schema.asClass(Schema.String) { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } } strictEqual(A.decodeUnknownSync("a"), "a") }) it("static property", () => { class A extends Schema.asClass(Schema.String) { static readonly decodeUnknownSync = Schema.decodeUnknownSync(this) } strictEqual(A.decodeUnknownSync("a"), "a") }) it("static property using Schema.suspend", () => { class A extends Schema.asClass(Schema.String) { static readonly decodeUnknownSync = Schema.decodeUnknownSync(Schema.suspend(() => this)) } strictEqual(A.decodeUnknownSync("a"), "a") }) it("wrapping a Struct schema", () => { const struct = Schema.Struct({ name: Schema.String }) class A extends Schema.asClass(struct) { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } } deepStrictEqual(A.decodeUnknownSync({ name: "a" }), { name: "a" }) strictEqual(A.fields, struct.fields) }) it("subclassing (double wrap)", () => { class A extends Schema.asClass(Schema.FiniteFromString) { static get decodeUnknownSync() { return Schema.decodeUnknownSync(this) } } class B extends A { static encodeSync = Schema.encodeSync(this) } strictEqual(B.decodeUnknownSync("1"), 1) strictEqual(B.encodeSync(1), "1") }) }) }) describe("resolveAnnotations", () => { it("returns undefined for a schema without annotations", () => { strictEqual(Schema.resolveAnnotations(Schema.String), undefined) }) it("returns annotations from the base schema", () => { const schema = Schema.String.annotate({ title: "my string" }) deepStrictEqual(Schema.resolveAnnotations(schema), { title: "my string" }) }) it("returns annotations from the last check", () => { const schema = Schema.String .annotate({ title: "base" }) .check(Schema.isNonEmpty().annotate({ title: "check" })) strictEqual(Schema.resolveAnnotations(schema)?.title, "check") }) }) describe("resolveAnnotationsKey", () => { it("returns undefined for a schema without key annotations", () => { strictEqual(Schema.resolveAnnotationsKey(Schema.String), undefined) }) it("returns key annotations", () => { const schema = Schema.String.annotateKey({ messageMissingKey: "required" }) deepStrictEqual(Schema.resolveAnnotationsKey(schema), { messageMissingKey: "required" }) }) })