Files

9864 lines
308 KiB
TypeScript

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>("A")(Schema.Struct({
a: Schema.String
})) {}
class B extends Schema.Opaque<B>()(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": <transformation> }`, 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"?: <transformation> }`, 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 <redacted>
at ["value"]`
)
await decoding.fail(
Redacted.make(1.2),
`Invalid data <redacted>
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 <redacted>
at ["value"]`
)
await encoding.fail(
Redacted.make(1.2),
`Invalid data <redacted>
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 <redacted>
at ["value"]`
)
await decoding.fail(
Redacted.make("a"),
`Invalid data <redacted>
at ["value"]`
)
await decoding.fail(
Redacted.make("1.2"),
`Invalid data <redacted>
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 <redacted>
at ["value"]`
)
await encoding.fail(
Redacted.make("a"),
`Invalid data <redacted>
at ["value"]`
)
await encoding.fail(
Redacted.make(1.2),
`Invalid data <redacted>
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 <redacted:password>
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 <redacted:password>
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 <redacted>`)
})
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 <redacted>`)
await decoding.fail("1.2", `Invalid data <redacted>`)
})
})
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 = <S>(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<A, T> {
readonly a: A
readonly categories: ReadonlyArray<T>
}
interface CategoryType extends Category<number, CategoryType> {}
interface CategoryEncoded extends Category<string, CategoryEncoded> {}
const schema = Schema.Struct({
a: Schema.FiniteFromString.check(Schema.isGreaterThan(0)),
categories: Schema.Array(Schema.suspend((): Schema.Codec<CategoryType, CategoryEncoded> => 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<never>))
})
})
})
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>("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>("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<never>))
})
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<never>))
})
})
})
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>("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>("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")({
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, { value: Effect.Effect<number> }>()("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<void>()
const secondCompleted = yield* Deferred.make<void>()
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<void>()
const secondCompleted = yield* Deferred.make<void>()
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<void>()
const secondCompleted = yield* Deferred.make<void>()
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<void>()
const firstLatch = yield* Deferred.make<void>()
const secondStarted = yield* Deferred.make<void>()
const secondInterrupted = yield* Deferred.make<void>()
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<A>()(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("<h1>")
await decoding.succeed("<h2>")
await decoding.fail(
"<h3>",
`Expected a string matching template literal parts, got "<h3>"`
)
})
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>", ["<", "h1", ">"])
await decoding.succeed("<h2>", ["<", "h2", ">"])
await decoding.fail(
"<h3>",
`Expected a string matching template literal parts, got "<h3>"`
)
})
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("<h1>", ["<", ["h", 1], ">"])
await decoding.succeed("<h2>", ["<", ["h", 2], ">"])
await decoding.fail(
"<h3>",
`Expected a string matching template literal parts, got "h3"
at [1]`
)
})
})
describe("Class", () => {
it("make with void input", () => {
class A extends Schema.Class<A>("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>("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")({
a: Schema.String
}) {}
assertTrue(A.ast === A.ast)
})
it("should set the identifier annotation", () => {
class A extends Schema.Class<A>("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")({
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")({
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")({
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, { readonly brand: unique symbol }>("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, { readonly brand: unique symbol }>("A")(Schema.Struct({
a: Schema.String
})) {}
class B extends Schema.Class<B, { readonly brand: unique symbol }>("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")({
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")({
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>("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")({
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")({
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")({
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")({
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>("A")(Schema.Struct({
a: Schema.String
})) {
readonly _a = 1
}
class B extends A.extend<B>("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")({
a: Schema.String
}) {}
class B extends A.extend<B>("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")({
a: Schema.String
}) {}
class B extends A.extend<B>("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")({
a: Schema.String
}) {}
class B extends A.extend<B>("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>("A")(
Schema.Struct({
a: Schema.Number
}).check(Schema.makeFilter(({ a }) => a > 0, { expected: "positive a" }))
) {}
class B extends A.extend<B>("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")({
a: Schema.String
}) {
static readonly aStatic = "value"
}
class B extends A.extend<B, typeof A>("B")({
b: Schema.Number
}) {}
strictEqual(B.aStatic, "value")
})
})
})
describe("TaggedClass", () => {
it("make with void input", () => {
class A extends Schema.TaggedClass<A>()("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<A>("B")("A", {
a: Schema.String
}) {}
strictEqual(A.identifier, "B")
})
it("fields argument", async () => {
class A extends Schema.TaggedClass<A>()("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", {
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>()(
"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", {
a: Schema.String
}) {}
class B extends A.extend<B>("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>("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>("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>("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>("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>("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")({
a: Schema.String
}) {
readonly _a = 1
}
class B extends A.extend<B>("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>("A")({
message: Schema.String
}) {}
class B extends A.extend<B>("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>("A")({
message: Schema.String
}) {}
class B extends A.extend<B>("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>("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>()("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>()("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>()("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>()(
"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<E>()("TaggedErrorName", {
id: Schema.Number
}) {}
const err = new E({ id: 1 })
strictEqual(err.name, "TaggedErrorName")
})
it("name matches identifier", () => {
class E extends Schema.TaggedErrorClass<E>("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<E>("A")("B", {
a: Schema.Number
}) {}
class E2 extends E.extend<E2>("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>()("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", {
a: Schema.String
}) {}
class B extends A.extend<B>("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", {
a: Schema.String
}) {}
class B extends A.extend<B>("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", {
a: Schema.String
}) {}
class B extends A.extend<B>("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, { fallback: Effect.Effect<string> }>()("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, { fallback: Effect.Effect<number> }>()("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, { fallback: Effect.Effect<number> }>()("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, { fallback: Effect.Effect<string> }>()("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, { fallback: Effect.Effect<string> }>()("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<never>))
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<never>))
})
})
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<never>))
})
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<never>))
})
})
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<never>))
})
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<never>))
})
})
})
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<never>))
})
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<never>))
})
})
})
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<never>))
})
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<never>))
})
})
})
describe("decodeUnknownResult", () => {
it("should throw on async decoding", () => {
const AsyncString = Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter((os: Option.Option<string>) =>
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, number>()("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<string>) =>
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, number>()("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")({
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 <filter>, 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>("A")({
_tag: Schema.tag("A"),
a: Schema.String
}) {}
class B extends Schema.Class<B>("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, { fallback: Effect.Effect<string> }>()("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, { fallback: Effect.Effect<string> }>()("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, { fallback: Effect.Effect<number> }>()("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, { fallback: Effect.Effect<number> }>()("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<string & Brand.Brand<"a">>()))
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<Int>(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<Int>(Schema.isInt())
type Positive = number & Brand.Brand<"Positive">
const Positive = Brand.check<Positive>(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" })
})
})