Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'

This commit is contained in:
-Puter
2026-07-19 03:28:54 +05:30
parent 2daf979036
commit a37e0cc3c9
2163 changed files with 668421 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
import { Exit, Option } from "effect"
import { describe, expect, it, vi } from "vitest"
const SCHEMA_MODULE_PATH = "../../src/Schema.ts"
describe("HMR", () => {
it("sanity check: reload produces distinct constructors", async () => {
const PATH = "./fixtures/HMR-sanity-check.ts"
const mod1: any = await vi.importActual(PATH)
vi.resetModules()
const mod2: any = await vi.importActual(PATH)
const a = new mod1.A("a")
expect(a instanceof mod1.A).toBe(true)
expect(a instanceof mod2.A).toBe(false)
})
it("isAST", async () => {
const SCHEMA_AST_MODULE_PATH = "../../src/SchemaAST.ts"
const mod1: any = await vi.importActual(SCHEMA_AST_MODULE_PATH)
vi.resetModules()
const mod2: any = await vi.importActual(SCHEMA_AST_MODULE_PATH)
const isAST = mod1.isAST
const b = mod2.unknown
expect(isAST(b)).toBe(true)
})
it("isSchema", async () => {
const mod1: any = await vi.importActual(SCHEMA_MODULE_PATH)
vi.resetModules()
const mod2: any = await vi.importActual(SCHEMA_MODULE_PATH)
const isSchema = mod1.isSchema
const schema = mod2.Unknown
expect(isSchema(schema)).toBe(true)
})
it("isSchemaError", async () => {
const mod1: any = await vi.importActual(SCHEMA_MODULE_PATH)
vi.resetModules()
const mod2: any = await vi.importActual(SCHEMA_MODULE_PATH)
const exit = mod1.decodeUnknownExit(mod1.String)(null)
expect(Exit.isFailure(exit)).toBe(true)
const o: any = Exit.findErrorOption(exit)
expect(Option.isSome(o)).toBe(true)
const schemaError = o.value
expect(mod2.isSchemaError(schemaError)).toBe(true)
})
it("Schema.Class", async () => {
const PATH = "./fixtures/HMR-Class.ts"
const mod1: any = await vi.importActual(PATH)
vi.resetModules()
const mod2: any = await vi.importActual(PATH)
const schema: any = await vi.importActual(SCHEMA_MODULE_PATH)
const a = new mod1.A({ a: "a" })
expect(a instanceof mod1.A).toBe(true)
const b = new mod2.A({ a: "a" })
expect(b instanceof mod1.A).toBe(false)
expect(String(schema.encodeUnknownExit(mod1.A)(b))).toBe(`Success({"a":"a"})`)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,439 @@
import { Schema, SchemaAST, SchemaGetter, SchemaTransformation } from "effect"
import { runInNewContext } from "node:vm"
import { describe, it } from "vitest"
import { deepStrictEqual, doesNotThrow, strictEqual, throws } from "../utils/assert.ts"
describe("SchemaAST", () => {
it("isJson", () => {
strictEqual(SchemaAST.isJson(null), true)
strictEqual(SchemaAST.isJson(undefined), false)
strictEqual(SchemaAST.isJson(true), true)
strictEqual(SchemaAST.isJson(false), true)
strictEqual(SchemaAST.isJson("string"), true)
strictEqual(SchemaAST.isJson(1), true)
strictEqual(SchemaAST.isJson(1.5), true)
strictEqual(SchemaAST.isJson(1n), false)
strictEqual(SchemaAST.isJson(NaN), false)
strictEqual(SchemaAST.isJson(Infinity), false)
strictEqual(SchemaAST.isJson(-Infinity), false)
strictEqual(SchemaAST.isJson(Symbol.for("symbol")), false)
strictEqual(SchemaAST.isJson([]), true)
strictEqual(SchemaAST.isJson([1]), true)
strictEqual(SchemaAST.isJson([1, undefined]), false)
strictEqual(SchemaAST.isJson([1, 1n]), false)
strictEqual(SchemaAST.isJson({}), true)
strictEqual(SchemaAST.isJson({ a: 1 }), true)
strictEqual(SchemaAST.isJson({ a: undefined }), false)
strictEqual(SchemaAST.isJson({ a: 1, b: 1n }), false)
strictEqual(SchemaAST.isJson(new Map([["a", 1]])), false)
strictEqual(SchemaAST.isJson(new Set([1])), false)
strictEqual(SchemaAST.isJson(new Date(0)), false)
strictEqual(SchemaAST.isJson(/a/), false)
strictEqual(SchemaAST.isJson(new Uint8Array([1])), false)
class A {
readonly a = 1
}
strictEqual(SchemaAST.isJson(new A()), false)
const nullPrototype: Record<string, unknown> = Object.create(null)
nullPrototype.a = { b: [1, true, null] }
strictEqual(SchemaAST.isJson(nullPrototype), true)
const crossRealmRecord: unknown = runInNewContext("({ a: [1, true, null] })")
strictEqual(SchemaAST.isJson(crossRealmRecord), true)
const crossRealmClass: unknown = runInNewContext("new (class A { a = 1 })()")
strictEqual(SchemaAST.isJson(crossRealmClass), false)
// nested
strictEqual(SchemaAST.isJson({ a: { b: 1 } }), true)
strictEqual(SchemaAST.isJson({ a: [1, { b: "c" }] }), true)
strictEqual(SchemaAST.isJson({ a: { b: 1n } }), false)
// circular reference
const circular: Record<string, unknown> = {}
circular.self = circular
strictEqual(SchemaAST.isJson(circular), false)
// accepts DAGs
const shared = { a: 1 }
strictEqual(SchemaAST.isJson({ x: shared, y: shared }), true)
strictEqual(SchemaAST.isJson([shared, { nested: shared }]), true)
// Nested DAG
const deeper = { parent: { left: shared, right: shared } }
strictEqual(SchemaAST.isJson(deeper), true)
})
it("Schema.toCodecJson rejects non-JSON objects", () => {
const encode = Schema.encodeUnknownExit(Schema.toCodecJson(Schema.Unknown))
strictEqual(encode(new Map([["a", 1]]))._tag, "Failure")
})
it("isStringTree", () => {
strictEqual(SchemaAST.isStringTree(undefined), true)
strictEqual(SchemaAST.isStringTree("string"), true)
strictEqual(SchemaAST.isStringTree(null), false)
strictEqual(SchemaAST.isStringTree(true), false)
strictEqual(SchemaAST.isStringTree(false), false)
strictEqual(SchemaAST.isStringTree(1), false)
strictEqual(SchemaAST.isStringTree(1n), false)
strictEqual(SchemaAST.isStringTree(Symbol.for("symbol")), false)
strictEqual(SchemaAST.isStringTree([]), true)
strictEqual(SchemaAST.isStringTree(["a"]), true)
strictEqual(SchemaAST.isStringTree(["a", undefined]), true)
strictEqual(SchemaAST.isStringTree(["a", 1]), false)
strictEqual(SchemaAST.isStringTree({}), true)
strictEqual(SchemaAST.isStringTree({ a: "b" }), true)
strictEqual(SchemaAST.isStringTree({ a: undefined }), true)
strictEqual(SchemaAST.isStringTree({ a: "b", c: 1 }), false)
// nested
strictEqual(SchemaAST.isStringTree({ a: { b: "c" } }), true)
strictEqual(SchemaAST.isStringTree({ a: ["b", { c: "d" }] }), true)
strictEqual(SchemaAST.isStringTree({ a: { b: 1 } }), false)
// circular reference
const circular: Record<string, unknown> = {}
circular.self = circular
strictEqual(SchemaAST.isStringTree(circular), false)
})
describe("toType", () => {
it("promotes encodingChecks when contained type shape is preserved", () => {
const schema = Schema.Struct({ a: Schema.String }).pipe(
Schema.flip,
Schema.check(Schema.makeFilter((o) => o.a.length > 1)),
Schema.flip
)
const ast = SchemaAST.toType(schema.ast)
strictEqual(SchemaAST.isObjects(ast), true)
strictEqual(ast.checks?.length, 1)
strictEqual(ast.encodingChecks, undefined)
})
it("drops encodingChecks when contained type shape changes", () => {
const schema = Schema.Struct({ a: Schema.FiniteFromString }).pipe(
Schema.flip,
Schema.check(Schema.makeFilter((o) => o.a.length > 1)),
Schema.flip
)
const ast = SchemaAST.toType(schema.ast)
strictEqual(SchemaAST.isObjects(ast), true)
strictEqual(ast.checks, undefined)
strictEqual(ast.encodingChecks, undefined)
})
})
describe("collectSentinels", () => {
describe("Declaration", () => {
it("~sentinels", () => {
class A {
readonly _tag = "A"
}
const schema = Schema.instanceOf(A, { "~sentinels": [{ key: "_tag", literal: "A" }] })
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
})
})
describe("Struct", () => {
it("required tag", () => {
const schema = Schema.Struct({
_tag: Schema.Literal("a"),
a: Schema.String
})
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "a" }])
})
it("optional tag", () => {
const schema = Schema.Struct({
_tag: Schema.optionalKey(Schema.Literal("a")),
a: Schema.String
})
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [])
})
})
describe("Tuple", () => {
it("required element", () => {
const schema = Schema.Tuple([Schema.Literal("a"), Schema.Number])
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: 0, literal: "a" }])
})
it("optional element", () => {
const schema = Schema.Tuple([Schema.Number, Schema.optionalKey(Schema.Literal("a"))])
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [])
})
})
it("Declaration", () => {
class A {
readonly _tag = "A"
}
const schema = Schema.instanceOf(
A,
{ "~sentinels": [{ key: "_tag", literal: "A" }] }
)
const ast = schema.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
})
it("Class", () => {
class A extends Schema.Class<A>("A")({
type: Schema.Literal("A"),
a: Schema.String
}) {}
const ast = A.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "type", literal: "A" }])
})
it("TaggedClass", () => {
class A extends Schema.TaggedClass<A>()("A", {
a: Schema.String
}) {}
const ast = A.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }])
})
it("ErrorClass", () => {
class E extends Schema.ErrorClass<E>("E")({
type: Schema.Literal("E"),
e: Schema.String
}) {}
const ast = E.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "type", literal: "E" }])
})
it("TaggedErrorClass", () => {
class E extends Schema.TaggedErrorClass<E>()("E", {
e: Schema.String
}) {}
const ast = E.ast
deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "E" }])
})
})
describe("getCandidates", () => {
it("should exclude never", () => {
const schema = Schema.Union([Schema.String, Schema.Never])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates("a", ast.types), [ast.types[0]])
})
it("should exclude by type", () => {
const schema = Schema.NullishOr(Schema.String)
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates("a", ast.types), [ast.types[0]])
deepStrictEqual(SchemaAST.getCandidates(null, ast.types), [ast.types[1]])
deepStrictEqual(SchemaAST.getCandidates(undefined, ast.types), [ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates(1, ast.types), [])
})
it("should exclude by literals", () => {
const schema = Schema.Union([
Schema.UniqueSymbol(Symbol.for("a")),
Schema.Literal("b"),
Schema.String
])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates(Symbol.for("a"), ast.types), [ast.types[0]])
deepStrictEqual(SchemaAST.getCandidates("b", ast.types), [ast.types[1], ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates("c", ast.types), [ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates(1, ast.types), [])
deepStrictEqual(SchemaAST.getCandidates(undefined, ast.types), [])
})
it("Literals", () => {
const schema = Schema.Literals(["a", "b", "c"])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates("a", ast.types), [ast.types[0]])
deepStrictEqual(SchemaAST.getCandidates("b", ast.types), [ast.types[1]])
deepStrictEqual(SchemaAST.getCandidates("c", ast.types), [ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates("d", ast.types), [])
deepStrictEqual(SchemaAST.getCandidates(null, ast.types), [])
deepStrictEqual(SchemaAST.getCandidates(undefined, ast.types), [])
})
it("String | Literals", () => {
const schema = Schema.Union([Schema.String, Schema.Literals(["a", "b", "c"])])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates(undefined, ast.types), [])
})
it("should handle tagged structs", () => {
const schema = Schema.Union([
Schema.Struct({ _tag: Schema.tag("a"), a: Schema.String }),
Schema.Struct({ _tag: Schema.tag("b"), b: Schema.Number }),
Schema.String
])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates({}, ast.types), [])
deepStrictEqual(SchemaAST.getCandidates({ _tag: "a" }, ast.types), [ast.types[0]])
deepStrictEqual(SchemaAST.getCandidates({ _tag: "b" }, ast.types), [ast.types[1]])
deepStrictEqual(SchemaAST.getCandidates({ _tag: "c" }, ast.types), [])
deepStrictEqual(SchemaAST.getCandidates("", ast.types), [ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates(1, ast.types), [])
})
it("should collect matches from different sentinel keys without duplicates", () => {
const schema = Schema.Union([
Schema.Struct({
kind: Schema.Literal("a"),
status: Schema.Literal("ready"),
value: Schema.String
}),
Schema.Struct({ status: Schema.Literal("ready"), value: Schema.String })
])
const ast = schema.ast
deepStrictEqual(
SchemaAST.getCandidates({ kind: "a", status: "ready", value: "value" }, ast.types),
[ast.types[0], ast.types[1]]
)
})
it("should handle tagged tuples", () => {
const schema = Schema.Union([
Schema.Tuple([Schema.Literal("a"), Schema.String]),
Schema.Tuple([Schema.Literal("b"), Schema.Number]),
Schema.String
])
const ast = schema.ast
deepStrictEqual(SchemaAST.getCandidates([], ast.types), [])
deepStrictEqual(SchemaAST.getCandidates(["a"], ast.types), [ast.types[0]])
deepStrictEqual(SchemaAST.getCandidates(["b"], ast.types), [ast.types[1]])
deepStrictEqual(SchemaAST.getCandidates(["c"], ast.types), [])
deepStrictEqual(SchemaAST.getCandidates("", ast.types), [ast.types[2]])
deepStrictEqual(SchemaAST.getCandidates(1, ast.types), [])
})
})
describe("getIndexSignatureKeys", () => {
it("String", () => {
const sym = Symbol.for("sym")
const input: { readonly [x: PropertyKey]: number } = { a: 1, b: 2, [sym]: 3 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.String.ast, SchemaAST.defaultParseOptions), [
"a",
"b"
])
})
it("String with checks", () => {
const input = { a: 1, ab: 2, b: 3 }
deepStrictEqual(
SchemaAST.getIndexSignatureKeys(
input,
Schema.String.check(Schema.isPattern(/^a/)).ast,
SchemaAST.defaultParseOptions
),
["a", "ab"]
)
})
it("transformed String with decoded checks", () => {
const schema = Schema.String.pipe(Schema.decode(SchemaTransformation.snakeToCamel())).check(
Schema.isPattern(/^aB$/)
)
const input = { a_b: 1, x_y: 2 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), [
"a_b",
"x_y"
])
})
it("TemplateLiteral", () => {
const schema = Schema.TemplateLiteral(["a"])
const input = { a: 1, ab: 2, b: 3 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), ["a"])
})
it("TemplateLiteral with checked parts", () => {
const schema = Schema.TemplateLiteral(["a", Schema.NonEmptyString])
const input = { a: 1, ab: 2, b: 3 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), ["ab"])
})
it("Symbol", () => {
const a = Symbol.for("a")
const b = Symbol.for("b")
const input: { readonly [x: PropertyKey]: number } = { c: 1, [a]: 2, [b]: 3 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Symbol.ast, SchemaAST.defaultParseOptions), [a, b])
})
it("Number", () => {
const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Number.ast, SchemaAST.defaultParseOptions), [
"1",
"1.5",
"-2",
"NaN"
])
})
it("Number with checks", () => {
const input = { "1": 1, "1.5": 2, "-2": 3, a: 4, NaN: 5 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, Schema.Int.ast, SchemaAST.defaultParseOptions), [
"1",
"-2"
])
})
it("Union", () => {
const schema = Schema.Union([Schema.Symbol, Schema.Number])
const sym = Symbol.for("sym")
const input: { readonly [x: PropertyKey]: number } = { "1": 1, b: 2, [sym]: 3 }
deepStrictEqual(SchemaAST.getIndexSignatureKeys(input, schema.ast, SchemaAST.defaultParseOptions), [sym, "1"])
})
})
describe("record", () => {
it("treats Never parameters as no keys", () => {
const ast = SchemaAST.record(Schema.Never.ast, Schema.Number.ast, undefined)
deepStrictEqual(ast.propertySignatures, [])
deepStrictEqual(ast.indexSignatures, [])
})
it("ignores Never arms in union parameters", () => {
const ast = SchemaAST.record(Schema.Union([Schema.String, Schema.Never]).ast, Schema.Number.ast, undefined)
const indexSignature = ast.indexSignatures[0]!
deepStrictEqual(ast.propertySignatures, [])
strictEqual(ast.indexSignatures.length, 1)
strictEqual(indexSignature.parameter, Schema.String.ast)
strictEqual(indexSignature.type, Schema.Number.ast)
})
})
describe("IndexSignature", () => {
it("accepts valid parameters on both type and encoded side", () => {
doesNotThrow(() => new SchemaAST.IndexSignature(Schema.String.ast, Schema.Number.ast, undefined))
doesNotThrow(() => new SchemaAST.IndexSignature(Schema.NumberFromString.ast, Schema.Number.ast, undefined))
doesNotThrow(() =>
new SchemaAST.IndexSignature(
Schema.Union([Schema.String, Schema.NumberFromString]).ast,
Schema.Number.ast,
undefined
)
)
})
it("rejects invalid type side parameters", () => {
throws(
() => new SchemaAST.IndexSignature(Schema.Literal("a").ast, Schema.Number.ast, undefined),
new Error("Invalid index signature parameter Literal")
)
})
it("rejects invalid encoded side parameters", () => {
const StringFromBoolean = Schema.Boolean.pipe(
Schema.decodeTo(Schema.String, {
decode: SchemaGetter.transform((b: boolean) => globalThis.String(b)),
encode: SchemaGetter.transform((s: string) => s === "true")
})
)
throws(
() => new SchemaAST.IndexSignature(StringFromBoolean.ast, Schema.Number.ast, undefined),
new Error("Invalid index signature parameter String")
)
})
})
})

View File

@@ -0,0 +1,441 @@
import { assert } from "@effect/vitest"
import { DateTime, Effect, Option, Result, SchemaGetter } from "effect"
import { describe, it } from "vitest"
import { assertSome, deepStrictEqual } from "../utils/assert.ts"
function makeAsserts<T, E>(getter: SchemaGetter.Getter<T, E>) {
return async (input: E, expected: T) => {
const r = await Effect.runPromise(
getter.run(Option.some(input), {}).pipe(
Effect.mapError((issue) => issue.toString()),
Effect.result
)
)
deepStrictEqual(r, Result.succeed(Option.some(expected)))
}
}
describe("SchemaGetter", () => {
it("map", () => {
const getter = SchemaGetter.succeed(1).map((t) => t + 1)
const result = Effect.runSync(getter.run(Option.some(1), {}))
assertSome(result, 2)
})
it("dateTimeUtcFromInput", async () => {
const decoding = makeAsserts(SchemaGetter.dateTimeUtcFromInput<string>())
await decoding("2024-01-01 01:00:00", DateTime.makeUnsafe("2024-01-01T01:00:00.000Z"))
await decoding("2020-02-01T11:17:00+1100", DateTime.makeUnsafe("2020-02-01T00:17:00.000Z"))
// should support strings with explicit GMT zone
await decoding("Tue, 27 Jan 2026 17:14:06 GMT", DateTime.makeUnsafe("2026-01-27T17:14:06.000Z"))
})
describe("makeTreeRecord", () => {
it("reinitializes own undefined values before descending", () => {
deepStrictEqual(
SchemaGetter.makeTreeRecord([
["a", undefined],
["a[b]", 1]
]),
{ a: { b: 1 } }
)
})
it("reinitializes own undefined values at numeric indexes", () => {
deepStrictEqual(
SchemaGetter.makeTreeRecord([
["a[0]", undefined],
["a[0][b]", 1]
]),
{ a: [{ b: 1 }] }
)
})
})
describe("decodeFormData / encodeFormData", () => {
const decoding = makeAsserts(SchemaGetter.decodeFormData())
const encoding = makeAsserts(SchemaGetter.encodeFormData())
it("should support multiple values for the same key", async () => {
const formData = new FormData()
formData.append("a", "1")
formData.append("a", "2")
const object = {
a: ["1", "2"]
}
await decoding(formData, object)
await encoding(object, formData)
})
it("should handle top level empty keys", async () => {
const formData = new FormData()
formData.append("", "value")
const object = { "": "value" }
await decoding(formData, object)
await encoding(object, formData)
})
it("decodes simple top-level keys", async () => {
const formData = new FormData()
formData.append("a", "1")
formData.append("b", "two")
const object = {
a: "1",
b: "two"
}
await decoding(formData, object)
await encoding(object, formData)
})
it("decodes nested objects via bracket notation", async () => {
const formData = new FormData()
formData.append("user[name]", "John")
formData.append("user[email]", "john@example.com")
const object = {
user: {
name: "John",
email: "john@example.com"
}
}
await decoding(formData, object)
await encoding(object, formData)
})
it("decodes nested objects via dot notation", async () => {
const formData = new FormData()
formData.append("user.name", "John")
formData.append("user.email", "john@example.com")
const object = {
user: {
name: "John",
email: "john@example.com"
}
}
await decoding(formData, object)
})
it("decodes mixed dot + bracket notation", async () => {
const formData = new FormData()
formData.append("user.address[city]", "Milan")
formData.append("user.address[zip]", "20100")
const object = {
user: {
address: {
city: "Milan",
zip: "20100"
}
}
}
await decoding(formData, object)
})
it("decodes arrays with numeric indices", async () => {
const formData = new FormData()
formData.append("items[0]", "item1")
formData.append("items[1]", "item2")
const object = {
items: ["item1", "item2"]
}
await decoding(formData, object)
{
const formData = new FormData()
formData.append("items", "item1")
formData.append("items", "item2")
await encoding(object, formData)
}
})
it("decodes arrays with numeric indices and nested objects", async () => {
const formData = new FormData()
formData.append("items[0][id]", "a")
formData.append("items[0][name]", "Item A")
formData.append("items[1][id]", "b")
formData.append("items[1][name]", "Item B")
const object = {
items: [
{ id: "a", name: "Item A" },
{ id: "b", name: "Item B" }
]
}
await decoding(formData, object)
await encoding(object, formData)
})
it("decodes arrays with [] (append)", async () => {
const formData = new FormData()
formData.append("tags[]", "a")
formData.append("tags[]", "b")
formData.append("tags[]", "c")
const object = {
tags: ["a", "b", "c"]
}
await decoding(formData, object)
{
const formData = new FormData()
formData.append("tags", "a")
formData.append("tags", "b")
formData.append("tags", "c")
await encoding(object, formData)
}
})
it("decodes arrays with [] and nested objects", async () => {
const formData = new FormData()
formData.append("items[][id]", "x")
formData.append("items[][id]", "y")
const object = {
items: [
{ id: "x" },
{ id: "y" }
]
}
await decoding(formData, object)
})
it("decodes mixed indexed and append arrays under the same key", async () => {
const formData = new FormData()
formData.append("items[0]", "a")
formData.append("items[]", "b")
formData.append("items[]", "c")
const object = {
items: ["a", "b", "c"]
}
// Implementation detail: first write at index 0, then pushes at 1 and 2
await decoding(formData, object)
})
it("decodes nested objects inside appended array elements", async () => {
const formData = new FormData()
formData.append("users[][name]", "John")
formData.append("users[][name]", "Alice")
const object = {
users: [
{ name: "John" },
{ name: "Alice" }
]
}
await decoding(formData, object)
})
it("decodes complex mixed structure", async () => {
const formData = new FormData()
formData.append("user[name]", "John")
formData.append("user[address][city]", "Milan")
formData.append("user[address][zip]", "20100")
formData.append("orders[0][id]", "o1")
formData.append("orders[0][total]", "10")
formData.append("orders[1][id]", "o2")
formData.append("orders[1][total]", "20")
formData.append("tags[0]", "a")
formData.append("tags[1]", "b")
const object = {
user: {
name: "John",
address: {
city: "Milan",
zip: "20100"
}
},
orders: [
{ id: "o1", total: "10" },
{ id: "o2", total: "20" }
],
tags: ["a", "b"]
}
await decoding(formData, object)
})
it("stores __proto__ paths as own properties", async () => {
const pollutedKey = "__effectSchemaPolluted"
Reflect.deleteProperty(Object.prototype, pollutedKey)
try {
const formData = new FormData()
formData.append(`__proto__[${pollutedKey}]`, "yes")
await decoding(formData, {
["__proto__"]: {
[pollutedKey]: "yes"
}
})
assert.isFalse(Object.hasOwn(Object.prototype, pollutedKey))
} finally {
Reflect.deleteProperty(Object.prototype, pollutedKey)
}
})
})
describe("decodeURLSearchParams / encodeURLSearchParams", () => {
const decoding = makeAsserts(SchemaGetter.decodeURLSearchParams())
const encoding = makeAsserts(SchemaGetter.encodeURLSearchParams())
it("should support multiple values for the same key", async () => {
const urlSearchParams = new URLSearchParams("a=1&a=2")
const object = {
a: ["1", "2"]
}
await decoding(urlSearchParams, object)
await encoding(object, urlSearchParams)
})
it("should handle top level empty keys", async () => {
const urlSearchParams = new URLSearchParams("=value")
const object = { "": "value" }
await decoding(urlSearchParams, object)
await encoding(object, urlSearchParams)
})
it("decodes simple top-level keys", async () => {
const urlSearchParams = new URLSearchParams("a=1&b=two")
const object = {
a: "1",
b: "two"
}
await decoding(urlSearchParams, object)
await encoding(object, urlSearchParams)
})
it("decodes nested objects via bracket notation", async () => {
const urlSearchParams = new URLSearchParams("user[name]=John&user[email]=john@example.com")
const object = {
user: {
name: "John",
email: "john@example.com"
}
}
await decoding(urlSearchParams, object)
await encoding(object, urlSearchParams)
})
it("decodes nested objects via dot notation", async () => {
const urlSearchParams = new URLSearchParams("user.name=John&user.email=john@example.com")
const object = {
user: {
name: "John",
email: "john@example.com"
}
}
await decoding(urlSearchParams, object)
})
it("decodes mixed dot + bracket notation", async () => {
const urlSearchParams = new URLSearchParams("user.address[city]=Milan&user.address[zip]=20100")
const object = {
user: {
address: {
city: "Milan",
zip: "20100"
}
}
}
await decoding(urlSearchParams, object)
})
it("decodes arrays with numeric indices", async () => {
const urlSearchParams = new URLSearchParams("items[0]=item1&items[1]=item2")
const object = {
items: ["item1", "item2"]
}
await decoding(urlSearchParams, object)
{
const urlSearchParams = new URLSearchParams("items=item1&items=item2")
await encoding(object, urlSearchParams)
}
})
it("decodes arrays with numeric indices and nested objects", async () => {
const urlSearchParams = new URLSearchParams(
"items[0][id]=a&items[0][name]=Item A&items[1][id]=b&items[1][name]=Item B"
)
const object = {
items: [
{ id: "a", name: "Item A" },
{ id: "b", name: "Item B" }
]
}
await decoding(urlSearchParams, object)
await encoding(object, urlSearchParams)
})
it("decodes arrays with [] (append)", async () => {
const urlSearchParams = new URLSearchParams("tags[]=a&tags[]=b&tags[]=c")
const object = {
tags: ["a", "b", "c"]
}
await decoding(urlSearchParams, object)
{
const urlSearchParams = new URLSearchParams("tags=a&tags=b&tags=c")
await encoding(object, urlSearchParams)
}
})
it("decodes arrays with [] and nested objects", async () => {
const urlSearchParams = new URLSearchParams("items[][id]=x&items[][id]=y")
const object = {
items: [{ id: "x" }, { id: "y" }]
}
await decoding(urlSearchParams, object)
})
it("decodes mixed indexed and append arrays under the same key", async () => {
const urlSearchParams = new URLSearchParams("items[0]=a&items[]=b&items[]=c")
const object = {
items: ["a", "b", "c"]
}
// Implementation detail: first write at index 0, then pushes at 1 and 2
await decoding(urlSearchParams, object)
})
it("decodes nested objects inside appended array elements", async () => {
const urlSearchParams = new URLSearchParams("users[][name]=John&users[][name]=Alice")
const object = {
users: [
{ name: "John" },
{ name: "Alice" }
]
}
await decoding(urlSearchParams, object)
})
it("decodes complex mixed structure", async () => {
const urlSearchParams = new URLSearchParams(
"user[name]=John&user.address[city]=Milan&user.address[zip]=20100&orders[0][id]=o1&orders[0][total]=10&orders[1][id]=o2&orders[1][total]=20&tags[0]=a&tags[1]=b"
)
const object = {
user: {
name: "John",
address: {
city: "Milan",
zip: "20100"
}
},
orders: [
{ id: "o1", total: "10" },
{ id: "o2", total: "20" }
],
tags: ["a", "b"]
}
await decoding(urlSearchParams, object)
})
it("does not traverse inherited constructor paths", async () => {
const pollutedKey = "__effectSchemaPolluted"
Reflect.deleteProperty(Object.prototype, pollutedKey)
try {
const urlSearchParams = new URLSearchParams(`constructor[prototype][${pollutedKey}]=yes`)
await decoding(urlSearchParams, {
constructor: {
prototype: {
[pollutedKey]: "yes"
}
}
})
assert.isFalse(Object.hasOwn(Object.prototype, pollutedKey))
} finally {
Reflect.deleteProperty(Object.prototype, pollutedKey)
}
})
})
})

View File

@@ -0,0 +1,9 @@
import { SchemaIssue } from "effect"
import { describe, it } from "vitest"
import { assertTrue } from "../utils/assert.ts"
describe("SchemaIssue", () => {
it("isIssue", () => {
assertTrue(SchemaIssue.isIssue(new SchemaIssue.MissingKey(undefined)))
})
})

View File

@@ -0,0 +1,323 @@
import { describe, it } from "@effect/vitest"
import { Cause, Effect, Exit, Option, Result, Schema, SchemaGetter, SchemaIssue, SchemaParser } from "effect"
import { assertTrue, strictEqual, throws } from "../utils/assert.ts"
describe("SchemaParser", () => {
const makeMixedCause = () =>
Cause.combine(
Cause.fail(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" })),
Cause.die(new Error("defect"))
)
const makeMixedSchemaErrorCause = () =>
Cause.combine(
Cause.fail(new Schema.SchemaError(new SchemaIssue.InvalidValue(Option.some("a"), { message: "schema issue" }))),
Cause.die(new Error("defect"))
)
describe("make", () => {
it("should throw an error when the input is invalid", () => {
const schema = Schema.String
throws(() => SchemaParser.make(schema)(null as any), (e) => {
assertTrue(e instanceof Error)
assertTrue(SchemaIssue.isIssue(e.cause))
strictEqual(e.message, "Expected string, got null")
})
})
it("should throw an error when the cause contains both an Issue and a defect", () => {
const schema = Schema.Struct({
a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedSchemaErrorCause())))
})
throws(() => SchemaParser.make(schema)({}), (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("should throw an error when the cause is not an Issue", () => {
const schema = Schema.Struct({
a: Schema.String.pipe(Schema.withConstructorDefault(Effect.die(new Error("make defect"))))
})
throws(() => SchemaParser.makeOption(schema)({}), (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 an Issue and a defect", () => {
const schema = Schema.Struct({
a: Schema.String.pipe(Schema.withConstructorDefault(Effect.failCause(makeMixedSchemaErrorCause())))
})
throws(() => SchemaParser.makeOption(schema)({}), (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("decodeUnknownSync / encodeUnknownSync", () => {
it("should throw an error when the input is invalid", () => {
const schema = Schema.String
throws(() => SchemaParser.decodeUnknownSync(schema)(null), (e) => {
assertTrue(e instanceof Error)
assertTrue(SchemaIssue.isIssue(e.cause))
strictEqual(e.message, "Expected string, got null")
})
throws(() => SchemaParser.encodeUnknownSync(schema)(null), (e) => {
assertTrue(e instanceof Error)
assertTrue(SchemaIssue.isIssue(e.cause))
strictEqual(e.message, "Expected string, got null")
})
})
it("should throw an error when the cause contains both an Issue and a defect", () => {
const decodeSchema = Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())),
encode: SchemaGetter.passthrough()
}))
const encodeSchema = Schema.String.pipe(Schema.encode({
decode: SchemaGetter.passthrough(),
encode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause()))
}))
throws(() => SchemaParser.decodeUnknownSync(decodeSchema)("a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Sync adapter can only throw schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
throws(() => SchemaParser.encodeUnknownSync(encodeSchema)("a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Sync adapter can only throw schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
})
})
describe("decodeUnknownPromise / encodeUnknownPromise", () => {
it("should reject with an error when the input is invalid", async () => {
const schema = Schema.String
const r1 = await SchemaParser.decodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail)
assertTrue(Result.isFailure(r1))
assertTrue(r1.failure instanceof Error)
assertTrue(SchemaIssue.isIssue(r1.failure.cause))
strictEqual(r1.failure.message, "Expected string, got null")
const r2 = await SchemaParser.encodeUnknownPromise(schema)(null).then(Result.succeed, Result.fail)
assertTrue(Result.isFailure(r2))
assertTrue(r2.failure instanceof Error)
assertTrue(SchemaIssue.isIssue(r2.failure.cause))
strictEqual(r2.failure.message, "Expected string, got null")
})
it("should reject with an error when the cause contains both an Issue and a defect", async () => {
const decodeSchema = Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())),
encode: SchemaGetter.passthrough()
}))
const encodeSchema = Schema.String.pipe(Schema.encode({
decode: SchemaGetter.passthrough(),
encode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause()))
}))
const r1 = await SchemaParser.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 issues")
assertTrue(Cause.hasDies(r1.failure.cause as Cause.Cause<never>))
const r2 = await SchemaParser.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 issues")
assertTrue(Cause.hasDies(r2.failure.cause as Cause.Cause<never>))
})
})
describe("decodeUnknownOption / encodeUnknownOption", () => {
it("should return none when the input is invalid", () => {
const schema = Schema.String
assertTrue(Option.isSome(SchemaParser.decodeUnknownOption(schema)("a")))
assertTrue(Option.isNone(SchemaParser.decodeUnknownOption(schema)(null)))
assertTrue(Option.isSome(SchemaParser.encodeUnknownOption(schema)("a")))
assertTrue(Option.isNone(SchemaParser.encodeUnknownOption(schema)(null)))
})
it("should throw an error when the cause is not an 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(() => SchemaParser.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(() => SchemaParser.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 an Issue and a defect", () => {
const decodeSchema = Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())),
encode: SchemaGetter.passthrough()
}))
const encodeSchema = Schema.String.pipe(Schema.encode({
decode: SchemaGetter.passthrough(),
encode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause()))
}))
throws(() => SchemaParser.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(() => SchemaParser.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("is", () => {
it("should return false when the input is invalid", () => {
const is = SchemaParser.is(Schema.String)
strictEqual(is("a"), true)
strictEqual(is(null), false)
})
it("should throw an error when the cause is not an Issue", () => {
const schema = Schema.declareConstructor<string>()(
[],
() => () => Effect.die(new Error("is defect"))
)
throws(() => SchemaParser.is(schema)("a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Type guard adapter can only return false for schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
})
it("should throw an error when the cause contains both an Issue and a defect", () => {
const schema = Schema.declareConstructor<string>()(
[],
() => () => Effect.failCause(makeMixedCause())
)
throws(() => SchemaParser.is(schema)("a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Type guard adapter can only return false for schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
})
})
describe("asserts", () => {
it("should throw an error when the cause is not an Issue", () => {
const schema = Schema.declareConstructor<string>()(
[],
() => () => Effect.die(new Error("assert defect"))
)
throws(() => SchemaParser.asserts(schema, "a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Assertion adapter can only throw schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
})
it("should throw an error when the cause contains both an Issue and a defect", () => {
const schema = Schema.declareConstructor<string>()(
[],
() => () => Effect.failCause(makeMixedCause())
)
throws(() => SchemaParser.asserts(schema, "a"), (e) => {
assertTrue(e instanceof Error)
strictEqual(e.message, "Assertion adapter can only throw schema issues")
assertTrue(Cause.hasDies(e.cause as Cause.Cause<never>))
})
})
})
describe("decodeUnknownResult / encodeUnknownResult", () => {
it("should throw an error when the cause is not an 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(() => SchemaParser.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(() => SchemaParser.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>))
})
})
it("should throw an error when the cause contains both an Issue and a defect", () => {
const decodeSchema = Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())),
encode: SchemaGetter.passthrough()
}))
const encodeSchema = Schema.String.pipe(Schema.encode({
decode: SchemaGetter.passthrough(),
encode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause()))
}))
throws(() => SchemaParser.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(() => SchemaParser.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("decodeUnknownExit", () => {
it("should preserve mixed causes in union candidates instead of trying later candidates", () => {
const schema = Schema.Union([
Schema.String.pipe(Schema.decode({
decode: new SchemaGetter.Getter(() => Effect.failCause(makeMixedCause())),
encode: SchemaGetter.passthrough()
})),
Schema.Literal("a")
])
const exit = SchemaParser.decodeUnknownExit(schema)("a")
assertTrue(Exit.isFailure(exit))
assertTrue(Exit.hasDies(exit))
const error = Cause.findError(exit.cause)
assertTrue(Result.isSuccess(error))
assertTrue(SchemaIssue.isIssue(error.success))
})
})
})

View File

@@ -0,0 +1,5 @@
import { Schema } from "effect"
export class A extends Schema.Class<A>("Class")({
a: Schema.String
}) {}

View File

@@ -0,0 +1,3 @@
export class A {
constructor(public readonly a: string) {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
import { SchemaRepresentation } from "effect"
import { describe, it } from "vitest"
import { deepStrictEqual, throws } from "../../utils/assert.ts"
describe("fromJsonSchemaMultiDocument", () => {
it("preserves root order and shares definitions", () => {
const document = SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas: [
{ $ref: "#/$defs/A" },
{ $ref: "#/$defs/A", description: "second" },
{ type: "array", items: { $ref: "#/$defs/A" } },
{ $ref: "#/$defs/A", description: "fourth" }
],
definitions: {
A: { type: "string", minLength: 1 }
}
})
const definition = {
_tag: "String" as const,
checks: [{ _tag: "Filter" as const, meta: { _tag: "isMinLength" as const, minLength: 1 } }]
}
deepStrictEqual(document, {
representations: [
{ _tag: "Reference", $ref: "A" },
{ ...definition, annotations: { description: "second" } },
{
_tag: "Arrays",
elements: [],
rest: [{ _tag: "Reference", $ref: "A" }],
checks: []
},
{ ...definition, annotations: { description: "fourth" } }
],
references: { A: definition }
})
})
it("resolves alias chains when combining a reference", () => {
const document = SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas: [{ $ref: "#/$defs/A", description: "root" }],
definitions: {
A: { $ref: "#/$defs/B" },
B: { $ref: "#/$defs/C" },
C: { type: "number" }
}
})
deepStrictEqual(document, {
representations: [{
_tag: "Number",
checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }],
annotations: { description: "root" }
}],
references: {
A: { _tag: "Reference", $ref: "B" },
B: { _tag: "Reference", $ref: "C" },
C: {
_tag: "Number",
checks: [{ _tag: "Filter", meta: { _tag: "isFinite" } }]
}
}
})
})
it("tracks recursive definitions independently", () => {
const document = SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas: [{ $ref: "#/$defs/A" }, { $ref: "#/$defs/B" }],
definitions: {
A: { $ref: "#/$defs/A" },
B: { $ref: "#/$defs/B" }
}
})
deepStrictEqual(document, {
representations: [
{ _tag: "Reference", $ref: "A" },
{ _tag: "Reference", $ref: "B" }
],
references: {
A: { _tag: "Suspend", thunk: { _tag: "Reference", $ref: "A" }, checks: [] },
B: { _tag: "Suspend", thunk: { _tag: "Reference", $ref: "B" }, checks: [] }
}
})
})
it("throws when a reference that must be resolved is missing", () => {
throws(
() =>
SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas: [{ $ref: "#/$defs/Missing", description: "resolve" }],
definitions: {}
}),
"Reference Missing not found"
)
})
it("throws when resolving a circular alias chain", () => {
throws(
() =>
SchemaRepresentation.fromJsonSchemaMultiDocument({
dialect: "draft-2020-12",
schemas: [{ $ref: "#/$defs/A", description: "resolve" }],
definitions: {
A: { $ref: "#/$defs/B" },
B: { $ref: "#/$defs/A" }
}
}),
"Circular reference detected: A"
)
})
})

View File

@@ -0,0 +1,39 @@
import { Schema, SchemaRepresentation } from "effect"
import { describe, it } from "vitest"
import { deepStrictEqual } from "../../utils/assert.ts"
describe("toJsonSchemaMultiDocument", () => {
it("should handle multiple schemas", () => {
const A = Schema.String.annotate({ identifier: "id", description: "a" })
const B = Schema.String.annotate({ identifier: "id", description: "b" })
const C = Schema.Tuple([A, B])
const multiDocument = SchemaRepresentation.fromASTs([A.ast, B.ast, C.ast])
const jsonMultiDocument = SchemaRepresentation.toJsonSchemaMultiDocument(multiDocument)
deepStrictEqual(jsonMultiDocument, {
dialect: "draft-2020-12",
schemas: [
{ "$ref": "#/$defs/id" },
{ "$ref": "#/$defs/id1" },
{
"type": "array",
"prefixItems": [
{ "$ref": "#/$defs/id" },
{ "$ref": "#/$defs/id1" }
],
"minItems": 2,
"maxItems": 2
}
],
definitions: {
id: {
"type": "string",
"description": "a"
},
id1: {
"type": "string",
"description": "b"
}
}
})
})
})

View File

@@ -0,0 +1,506 @@
import { Redacted, Schema, SchemaRepresentation } from "effect"
import { describe, it } from "vitest"
import { deepStrictEqual, strictEqual } from "../../utils/assert.ts"
describe("toSchema", () => {
function assertToSchemaRoundtrip(input: {
schema: Schema.Top
readonly reviver?: SchemaRepresentation.Reviver<Schema.Top> | undefined
}, runtime: string) {
const document = SchemaRepresentation.fromAST(input.schema.ast)
const roundtrip = SchemaRepresentation.fromAST(
SchemaRepresentation.toSchema(document, { reviver: input.reviver }).ast
)
deepStrictEqual(roundtrip, document)
const codeDocument = SchemaRepresentation.toCodeDocument(SchemaRepresentation.toMultiDocument(roundtrip))
strictEqual(codeDocument.codes[0].runtime, runtime)
}
describe("String", () => {
it("String", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String },
`Schema.String`
)
})
it("String & check", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isMinLength(1)) },
`Schema.String.check(Schema.isMinLength(1))`
)
})
describe("checks", () => {
it("isTrimmed", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isTrimmed()) },
`Schema.String.check(Schema.isTrimmed())`
)
})
it("isULID", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isULID()) },
`Schema.String.check(Schema.isULID())`
)
})
it("isGUID", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isGUID()) },
`Schema.String.check(Schema.isGUID())`
)
})
})
})
it("Struct", () => {
assertToSchemaRoundtrip(
{ schema: Schema.Struct({}) },
`Schema.Struct({ })`
)
assertToSchemaRoundtrip(
{ schema: Schema.Struct({ a: Schema.String }) },
`Schema.Struct({ "a": Schema.String })`
)
assertToSchemaRoundtrip(
{ schema: Schema.Struct({ [Symbol.for("a")]: Schema.String }) },
`Schema.Struct({ [_symbol]: Schema.String })`
)
assertToSchemaRoundtrip(
{ schema: Schema.Struct({ a: Schema.optionalKey(Schema.String) }) },
`Schema.Struct({ "a": Schema.optionalKey(Schema.String) })`
)
assertToSchemaRoundtrip(
{ schema: Schema.Struct({ a: Schema.mutableKey(Schema.String) }) },
`Schema.Struct({ "a": Schema.mutableKey(Schema.String) })`
)
assertToSchemaRoundtrip(
{ schema: Schema.Struct({ a: Schema.optionalKey(Schema.mutableKey(Schema.String)) }) },
`Schema.Struct({ "a": Schema.optionalKey(Schema.mutableKey(Schema.String)) })`
)
})
it("Record", () => {
assertToSchemaRoundtrip(
{ schema: Schema.Record(Schema.String, Schema.Number) },
`Schema.Record(Schema.String, Schema.Number)`
)
assertToSchemaRoundtrip(
{ schema: Schema.Record(Schema.Symbol, Schema.Number) },
`Schema.Record(Schema.Symbol, Schema.Number)`
)
})
it("StructWithRest", () => {
assertToSchemaRoundtrip(
{
schema: Schema.StructWithRest(Schema.Struct({ a: Schema.Number }), [
Schema.Record(Schema.String, Schema.Number)
])
},
`Schema.StructWithRest(Schema.Struct({ "a": Schema.Number }), [Schema.Record(Schema.String, Schema.Number)])`
)
})
it("Tuple", () => {
assertToSchemaRoundtrip(
{ schema: Schema.Tuple([]) },
`Schema.Tuple([])`
)
assertToSchemaRoundtrip(
{ schema: Schema.Tuple([Schema.String, Schema.Number]) },
`Schema.Tuple([Schema.String, Schema.Number])`
)
assertToSchemaRoundtrip(
{ schema: Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)]) },
`Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])`
)
})
it("Array", () => {
assertToSchemaRoundtrip(
{ schema: Schema.Array(Schema.String) },
`Schema.Array(Schema.String)`
)
})
it("TupleWithRest", () => {
assertToSchemaRoundtrip(
{ schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number]) },
`Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number])`
)
assertToSchemaRoundtrip(
{ schema: Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean]) },
`Schema.TupleWithRest(Schema.Tuple([Schema.String]), [Schema.Number, Schema.Boolean])`
)
})
it("Suspend", () => {
type Category = {
readonly name: string
readonly children: ReadonlyArray<Category>
}
const OuterCategory = Schema.Struct({
name: Schema.String,
children: Schema.Array(Schema.suspend((): Schema.Codec<Category> => OuterCategory))
}).annotate({ identifier: "Category" })
assertToSchemaRoundtrip(
{ schema: OuterCategory },
`Category`
)
})
describe("brand", () => {
it("brand", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.pipe(Schema.brand("a")) },
`Schema.String.pipe(Schema.brand("a"))`
)
})
it("brand & brand", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.pipe(Schema.brand("a"), Schema.brand("b")) },
`Schema.String.pipe(Schema.brand("a"), Schema.brand("b"))`
)
})
it("check & brand", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")) },
`Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b"))`
)
})
it("brand & check & brand", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b")) },
`Schema.String.pipe(Schema.brand("a")).check(Schema.isMinLength(1)).pipe(Schema.brand("b"))`
)
})
it("check & brand & check", () => {
assertToSchemaRoundtrip(
{ schema: Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2)) },
`Schema.String.check(Schema.isMinLength(1)).pipe(Schema.brand("b")).check(Schema.isMaxLength(2))`
)
})
})
describe("toSchemaDefaultReviver", () => {
function assertToSchemaWithReviver(schema: Schema.Top, runtime: string) {
assertToSchemaRoundtrip({ schema, reviver: SchemaRepresentation.toSchemaDefaultReviver }, runtime)
}
it("Option", () => {
assertToSchemaWithReviver(
Schema.Option(Schema.String),
`Schema.Option(Schema.String)`
)
assertToSchemaWithReviver(
Schema.Option(Schema.URL),
`Schema.Option(Schema.URL)`
)
})
it("Result", () => {
assertToSchemaWithReviver(
Schema.Result(Schema.String, Schema.Number),
`Schema.Result(Schema.String, Schema.Number)`
)
})
it("Json", () => {
assertToSchemaWithReviver(
Schema.Json,
`Schema.Json`
)
})
it("MutableJson", () => {
assertToSchemaWithReviver(
Schema.MutableJson,
`Schema.MutableJson`
)
})
it("Redacted", () => {
assertToSchemaWithReviver(
Schema.Redacted(Schema.String),
`Schema.Redacted(Schema.String)`
)
})
it("Redacted options", () => {
const schema = Schema.Redacted(Schema.String, {
label: "password",
disallowJsonEncode: true
})
const document = SchemaRepresentation.fromAST(schema.ast)
const roundtrip = SchemaRepresentation.toSchema<typeof schema>(document, {
reviver: SchemaRepresentation.toSchemaDefaultReviver
})
const encode = Schema.encodeUnknownExit(Schema.toCodecJson(roundtrip))
strictEqual(
String(encode(Redacted.make("secret", { label: "password" }))),
`Failure(Cause([Fail(SchemaError(Cannot serialize Redacted with label: "password"))]))`
)
strictEqual(
String(encode(Redacted.make("secret", { label: "other" }))),
`Failure(Cause([Fail(SchemaError(Expected "password", got "other"
at ["label"]))]))`
)
})
it("CauseReason", () => {
assertToSchemaWithReviver(
Schema.CauseReason(Schema.String, Schema.Number),
`Schema.CauseReason(Schema.String, Schema.Number)`
)
})
it("Cause", () => {
assertToSchemaWithReviver(
Schema.Cause(Schema.String, Schema.Number),
`Schema.Cause(Schema.String, Schema.Number)`
)
})
it("Exit", () => {
assertToSchemaWithReviver(
Schema.Exit(Schema.String, Schema.Number, Schema.Boolean),
`Schema.Exit(Schema.String, Schema.Number, Schema.Boolean)`
)
})
it("ReadonlyMap", () => {
assertToSchemaWithReviver(
Schema.ReadonlyMap(Schema.String, Schema.Number),
`Schema.ReadonlyMap(Schema.String, Schema.Number)`
)
})
it("HashMap", () => {
assertToSchemaWithReviver(
Schema.HashMap(Schema.String, Schema.Number),
`Schema.HashMap(Schema.String, Schema.Number)`
)
})
it("Chunk", () => {
assertToSchemaWithReviver(
Schema.Chunk(Schema.String),
`Schema.Chunk(Schema.String)`
)
})
it("ReadonlySet", () => {
assertToSchemaWithReviver(
Schema.ReadonlySet(Schema.String),
`Schema.ReadonlySet(Schema.String)`
)
})
it("RegExp", () => {
assertToSchemaWithReviver(
Schema.RegExp,
`Schema.RegExp`
)
})
it("URL", () => {
assertToSchemaWithReviver(
Schema.URL,
`Schema.URL`
)
})
describe("Date", () => {
it("Date", () => {
assertToSchemaWithReviver(
Schema.Date,
`Schema.Date`
)
})
describe("checks", () => {
it("isDateValid", () => {
assertToSchemaWithReviver(
Schema.Date.check(Schema.isDateValid()),
`Schema.Date.check(Schema.isDateValid())`
)
})
it("isGreaterThanDate", () => {
assertToSchemaWithReviver(
Schema.Date.check(Schema.isGreaterThanDate(new Date(0))),
`Schema.Date.check(Schema.isGreaterThanDate(new Date(0)))`
)
})
it("isGreaterThanOrEqualToDate", () => {
assertToSchemaWithReviver(
Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0))),
`Schema.Date.check(Schema.isGreaterThanOrEqualToDate(new Date(0)))`
)
})
it("isLessThanDate", () => {
assertToSchemaWithReviver(
Schema.Date.check(Schema.isLessThanDate(new Date(0))),
`Schema.Date.check(Schema.isLessThanDate(new Date(0)))`
)
})
it("isLessThanOrEqualToDate", () => {
assertToSchemaWithReviver(
Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0))),
`Schema.Date.check(Schema.isLessThanOrEqualToDate(new Date(0)))`
)
})
})
})
it("Duration", () => {
assertToSchemaWithReviver(
Schema.Duration,
`Schema.Duration`
)
})
it("FormData", () => {
assertToSchemaWithReviver(
Schema.FormData,
`Schema.FormData`
)
})
it("URLSearchParams", () => {
assertToSchemaWithReviver(
Schema.URLSearchParams,
`Schema.URLSearchParams`
)
})
it("Uint8Array", () => {
assertToSchemaWithReviver(
Schema.Uint8Array,
`Schema.Uint8Array`
)
})
it("DateTime.Utc", () => {
assertToSchemaWithReviver(
Schema.DateTimeUtc,
`Schema.DateTimeUtc`
)
})
it("Error", () => {
assertToSchemaWithReviver(
Schema.Error(),
`Schema.Error()`
)
})
it("Error with stack", () => {
assertToSchemaWithReviver(
Schema.Error({ includeStack: true }),
`Schema.Error({"includeStack":true})`
)
})
it("Error with excluded cause", () => {
assertToSchemaWithReviver(
Schema.Error({ excludeCause: true }),
`Schema.Error({"excludeCause":true})`
)
})
it("Defect", () => {
assertToSchemaWithReviver(
Schema.Defect(),
`Schema.Json`
)
})
it("HashSet", () => {
assertToSchemaWithReviver(
Schema.HashSet(Schema.String),
`Schema.HashSet(Schema.String)`
)
})
it("BigDecimal", () => {
assertToSchemaWithReviver(
Schema.BigDecimal,
`Schema.BigDecimal`
)
})
it("TimeZoneOffset", () => {
assertToSchemaWithReviver(
Schema.TimeZoneOffset,
`Schema.TimeZoneOffset`
)
})
it("TimeZoneNamed", () => {
assertToSchemaWithReviver(
Schema.TimeZoneNamed,
`Schema.TimeZoneNamed`
)
})
it("TimeZone", () => {
assertToSchemaWithReviver(
Schema.TimeZone,
`Schema.TimeZone`
)
})
it("DateTimeZoned", () => {
assertToSchemaWithReviver(
Schema.DateTimeZoned,
`Schema.DateTimeZoned`
)
})
describe("ReadonlySet", () => {
it("ReadonlySet(String)", () => {
assertToSchemaWithReviver(
Schema.ReadonlySet(Schema.String),
`Schema.ReadonlySet(Schema.String)`
)
})
describe("checks", () => {
it("isMinSize", () => {
assertToSchemaWithReviver(
Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2)),
`Schema.ReadonlySet(Schema.String).check(Schema.isMinSize(2))`
)
})
it("isMaxSize", () => {
assertToSchemaWithReviver(
Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2)),
`Schema.ReadonlySet(Schema.String).check(Schema.isMaxSize(2))`
)
})
})
it("isSizeBetween", () => {
assertToSchemaWithReviver(
Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2)),
`Schema.ReadonlySet(Schema.String).check(Schema.isSizeBetween(2, 2))`
)
})
})
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,239 @@
import { Schema } from "effect"
import * as DateTime from "effect/DateTime"
import * as FastCheck from "effect/testing/FastCheck"
import { describe, it } from "vitest"
import { deepStrictEqual, strictEqual, throws } from "../utils/assert.ts"
/**
* This suite intentionally avoids re-testing generic JSON Patch behavior
* (pointer parsing, add/replace/remove mechanics, ordering, etc).
*
* It focuses on Schema.toDifferJsonPatch-specific guarantees:
* - reference preservation when patch is a no-op
* - reference replacement for root replace
* - immutability of inputs
* - schema-specific diff/patch encodings (Number / Date / Defect / DateTimeUtcFromMillis)
* - property-based roundtrip across a broad set of codecs
*/
function roundtrip<T, E>(codec: Schema.Codec<T, E>) {
const differ = Schema.toDifferJsonPatch(codec)
const arbitrary = Schema.toArbitrary(codec)
const arb = arbitrary.filter((v) => {
// avoid prototype-poisoning-ish values that aren't valid JSON-ish containers for patching
if (
typeof v === "object" &&
v !== null &&
(Object.getPrototypeOf(v) === null || Object.hasOwn(v as any, "__proto__"))
) {
return false
}
return true
})
FastCheck.assert(
FastCheck.property(arb, arb, (v1, v2) => {
const patch = differ.diff(v1, v2)
const patched = differ.patch(v1, patch)
// two invalid dates are not considered equal by deepStrictEqual
if (patched instanceof Date && v2 instanceof Date && Object.is(patched.getTime(), v2.getTime())) {
return
}
deepStrictEqual(patched, v2)
})
)
}
describe("Schema.toDifferJsonPatch", () => {
describe("structural guarantees", () => {
it("patch returns the same reference if nothing changed (no-op patch)", () => {
const schema = Schema.Struct({ a: Schema.String })
const differ = Schema.toDifferJsonPatch(schema)
const value = { a: "a" }
strictEqual(differ.patch(value, []), value)
})
it("root replace returns the provided reference (no clone)", () => {
const differ = Schema.toDifferJsonPatch(Schema.Any)
const newRef = { hello: "world" }
const out = differ.patch({ old: true }, [{ op: "replace", path: "", value: newRef }])
strictEqual(out, newRef)
deepStrictEqual(out, newRef)
})
it("immutability: patch does not mutate the input", () => {
const differ = Schema.toDifferJsonPatch(Schema.Any)
const oldValue = { a: { b: [1, 2, 3] } }
const snapshot = JSON.parse(JSON.stringify(oldValue))
const out = differ.patch(oldValue, [
{ op: "replace", path: "/a/b/1", value: 9 },
{ op: "remove", path: "/a/b/2" }
])
deepStrictEqual(oldValue, snapshot)
deepStrictEqual(out, { a: { b: [1, 9] } })
})
})
describe("codec-specific diff/patch semantics", () => {
it("Number: distinguishes 0 and -0; treats NaN as equal to NaN", () => {
const differ = Schema.toDifferJsonPatch(Schema.Number)
deepStrictEqual(differ.diff(0, -0), [{ op: "replace", path: "", value: -0 }])
deepStrictEqual(differ.diff(-0, 0), [{ op: "replace", path: "", value: 0 }])
deepStrictEqual(differ.diff(NaN, NaN), [])
deepStrictEqual(differ.diff(Infinity, Infinity), [])
deepStrictEqual(differ.diff(-Infinity, -Infinity), [])
deepStrictEqual(differ.patch(0, [{ op: "replace", path: "", value: -0 }]), -0)
deepStrictEqual(differ.patch(-0, [{ op: "replace", path: "", value: 0 }]), 0)
})
it("Date: encodes invalid Date as a string on diff", () => {
const differ = Schema.toDifferJsonPatch(Schema.Date)
deepStrictEqual(
differ.diff(new Date("1970-01-01T00:00:00.000Z"), new Date(NaN)),
[{ op: "replace", path: "", value: "Invalid Date" }]
)
})
it("Defect: diff encodes an Error to a plain object; patch decodes back to Error", () => {
const differ = Schema.toDifferJsonPatch(Schema.Defect())
deepStrictEqual(differ.diff("", new Error("b")), [{
op: "replace",
path: "",
value: { name: "Error", message: "b" }
}])
deepStrictEqual(
differ.patch("", [{
op: "replace",
path: "",
value: { name: "Error", message: "b" }
}]),
new Error("b")
)
})
it("DateTimeUtcFromMillis: diff uses millis and patch rehydrates DateTime", () => {
const differ = Schema.toDifferJsonPatch(Schema.DateTimeUtcFromMillis)
deepStrictEqual(
differ.diff(
DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"),
DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")
),
[]
)
deepStrictEqual(
differ.diff(
DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"),
DateTime.makeUnsafe("2021-01-02T00:00:00.000Z")
),
[{ op: "replace", path: "", value: 1609545600000 }]
)
deepStrictEqual(
differ.patch(
DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"),
[{ op: "replace", path: "", value: 1609545600000 }]
),
DateTime.makeUnsafe("2021-01-02T00:00:00.000Z")
)
})
})
describe("integration sanity checks", () => {
it("diff/patch works when root container kind changes", () => {
const differ = Schema.toDifferJsonPatch(Schema.Any)
deepStrictEqual(differ.diff([], {}), [{ op: "replace", path: "", value: {} }])
deepStrictEqual(differ.patch([], [{ op: "replace", path: "", value: {} }]), {})
})
it("patch throws when asked to do an invalid replace (replace requires existence)", () => {
const differ = Schema.toDifferJsonPatch(Schema.Any)
const doc = {}
throws(() => differ.patch(doc, [{ op: "replace", path: "/x", value: 1 }]))
})
})
describe("roundtrip (property-based)", () => {
it("patch(diff(a,b)) round-trips for a wide set of codecs", () => {
roundtrip(Schema.Any.annotate({
toArbitrary: () => (fc) => fc.json()
}))
roundtrip(Schema.String)
roundtrip(Schema.Number)
roundtrip(Schema.Boolean)
roundtrip(Schema.BigInt)
roundtrip(Schema.Symbol)
// includes edgey keys without re-testing pointer logic exhaustively
roundtrip(Schema.Struct({
a: Schema.String,
"-": Schema.NullOr(Schema.String),
"": Schema.String
}))
roundtrip(Schema.Record(Schema.String, Schema.Number))
roundtrip(Schema.StructWithRest(
Schema.Struct({
a: Schema.Number,
"-": Schema.Number,
"": Schema.Number
}),
[Schema.Record(Schema.String, Schema.Number)]
))
roundtrip(Schema.Tuple([Schema.String, Schema.Number]))
roundtrip(Schema.Array(Schema.Number))
roundtrip(Schema.TupleWithRest(
Schema.Tuple([Schema.Number]),
[Schema.String]
))
roundtrip(Schema.TupleWithRest(
Schema.Tuple([Schema.Number]),
[Schema.String, Schema.Boolean]
))
roundtrip(Schema.Union([Schema.String, Schema.Finite]))
roundtrip(Schema.Finite)
roundtrip(Schema.Date)
roundtrip(Schema.URL)
roundtrip(Schema.RegExp)
roundtrip(Schema.Duration)
roundtrip(Schema.DateTimeUtc)
roundtrip(Schema.DateValid)
roundtrip(Schema.Uint8Array)
roundtrip(Schema.PropertyKey)
roundtrip(Schema.Option(Schema.String))
roundtrip(Schema.Result(Schema.Number, Schema.String))
roundtrip(Schema.ReadonlyMap(Schema.String, Schema.Number))
roundtrip(Schema.Error())
roundtrip(Schema.Json)
roundtrip(Schema.Exit(Schema.Number, Schema.String, Schema.Json))
class A extends Schema.Class<A>("A")({ value: Schema.Number }) {}
class B extends Schema.Class<B>("B")({ a: A }) {}
roundtrip(B)
class E extends Schema.ErrorClass<E>("E")({ message: Schema.String }) {}
roundtrip(E)
})
})
})

View File

@@ -0,0 +1,538 @@
import { BigDecimal, DateTime, Duration, Equivalence, HashMap, Option, Redacted, Result, Schema } from "effect"
import { describe, it } from "vitest"
import { assertFalse, assertTrue, throws } from "../utils/assert.ts"
const Modulo2 = Schema.Number.annotate({
toEquivalence: (): Equivalence.Equivalence<number> => Equivalence.make((a, b) => a % 2 === b % 2)
})
const Modulo3 = Schema.Number.annotate({
toEquivalence: (): Equivalence.Equivalence<number> => Equivalence.make((a, b) => a % 3 === b % 3)
})
describe("toEquivalence", () => {
it("Never", () => {
throws(
() =>
Schema.toEquivalence(Schema.Struct({
a: Schema.Never
})),
`Unsupported AST Never
at ["a"]`
)
throws(
() =>
Schema.toEquivalence(Schema.Tuple([
Schema.Never
])),
`Unsupported AST Never
at [0]`
)
})
it("String", () => {
const schema = Schema.String
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence("a", "a"))
assertFalse(equivalence("a", "b"))
})
describe("Tuple", () => {
it("should fail on non-array inputs", () => {
const schema = Schema.Tuple([Schema.String, Schema.Number])
const equivalence = Schema.toEquivalence(schema)
assertFalse(equivalence(["a", 1], null as never))
})
it("empty", () => {
const schema = Schema.Tuple([])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence([], []))
})
it("required elements", () => {
const schema = Schema.Tuple([Schema.String, Schema.Number])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["a", 1], ["a", 1]))
assertFalse(equivalence(["a", 1], ["b", 1]))
})
it("optionalKey elements", () => {
const schema = Schema.Tuple([Schema.String, Schema.optionalKey(Schema.Number)])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["a", 1], ["a", 1]))
assertTrue(equivalence(["a"], ["a"]))
assertFalse(equivalence(["a", 1], ["b", 1]))
assertFalse(equivalence(["a"], ["b"]))
})
it("optional elements", () => {
const schema = Schema.Tuple([Schema.String, Schema.optional(Schema.Number)])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["a", 1], ["a", 1]))
assertTrue(equivalence(["a"], ["a"]))
assertTrue(equivalence(["a", undefined], ["a", undefined]))
assertFalse(equivalence(["a", 1], ["b", 1]))
assertFalse(equivalence(["a"], ["b"]))
assertFalse(equivalence(["a", undefined], ["b", undefined]))
})
})
it("Array", () => {
const schema = Schema.Array(Schema.String)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["a", "b", "c"], ["a", "b", "c"]))
assertFalse(equivalence(["a", "b", "c"], ["a", "b", "d"]))
assertFalse(equivalence(["a", "b", "c"], ["a", "b"]))
assertFalse(equivalence(["a", "b", "c"], ["a", "b", "c", "d"]))
})
it("TupleWithRest", () => {
const schema = Schema.TupleWithRest(Schema.Tuple([Schema.String, Schema.Number]), [Schema.String, Schema.Number])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["a", 1, 2], ["a", 1, 2]))
assertTrue(equivalence(["a", 1, "b", 2], ["a", 1, "b", 2]))
assertFalse(equivalence(["a", 1, 2], ["a", 2, 2]))
assertFalse(equivalence(["a", 1, 2], ["a", 1, 3]))
assertFalse(equivalence(["a", 1, "b", 2], ["c", 1, "b", 2]))
assertFalse(equivalence(["a", 1, "b", 2], ["a", 1, "c", 2]))
assertFalse(equivalence(["a", 1, "b", 2], ["a", 2, "b", 2]))
assertFalse(equivalence(["a", 1, "b", 2], ["a", 1, "b", 3]))
})
it("TupleWithRest with multiple post-rest elements", () => {
const schema = Schema.TupleWithRest(Schema.Tuple([Schema.String]), [
Schema.String,
Schema.Number,
Schema.Boolean,
Schema.String
])
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(["head", "tail", 1, true, "last"], ["head", "tail", 1, true, "last"]))
assertFalse(equivalence(["head", "tail", 1, true, "A"], ["head", "tail", 1, true, "B"]))
})
describe("Struct", () => {
it("should fail on non-record inputs", () => {
const schema = Schema.Struct({ a: Schema.String })
const equivalence = Schema.toEquivalence(schema)
assertFalse(equivalence({ a: "a" }, 1 as never))
})
it("empty", () => {
const schema = Schema.Struct({})
const equivalence = Schema.toEquivalence(schema)
const a = {}
assertTrue(equivalence(a, a))
assertTrue(equivalence({}, {})) // Now supports structural equality
})
it("required fields", () => {
const schema = Schema.Struct({
a: Schema.String,
b: Schema.Number
})
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "b", b: 1 }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 }))
})
it("symbol keys", () => {
const a = Symbol.for("a")
const b = Symbol.for("b")
const schema = Schema.Struct({
[a]: Schema.String,
[b]: Schema.Number
})
const equivalence = Schema.toEquivalence(schema)
assertTrue(
equivalence({ [a]: "a", [b]: 1 }, { [a]: "a", [b]: 1 })
)
assertFalse(
equivalence({ [a]: "a", [b]: 1 }, { [a]: "b", [b]: 1 })
)
assertFalse(
equivalence({ [a]: "a", [b]: 1 }, { [a]: "a", [b]: 2 })
)
})
it("optionalKey fields", () => {
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optionalKey(Schema.Number)
})
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 }))
assertTrue(equivalence({ a: "a" }, { a: "a" }))
assertFalse(equivalence({ a: "a" }, { a: "b" }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "b", b: 1 }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 }))
})
it("optional fields", () => {
const schema = Schema.Struct({
a: Schema.String,
b: Schema.optional(Schema.Number)
})
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: "a", b: 1 }, { a: "a", b: 1 }))
assertTrue(equivalence({ a: "a" }, { a: "a" }))
assertTrue(equivalence({ a: "a", b: undefined }, { a: "a", b: undefined }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "b", b: 1 }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: 2 }))
assertFalse(equivalence({ a: "a", b: 1 }, { a: "a", b: undefined }))
assertFalse(equivalence({ a: "a", b: undefined }, { a: "a", b: 1 }))
})
})
describe("Record", () => {
it("Record(String, Number)", () => {
const schema = Schema.Record(Schema.String, Schema.Number)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: 1, b: 2 }, { a: 1, b: 2 }))
assertFalse(equivalence({ a: 1, b: 2 }, { a: 1, b: 3 }))
assertFalse(equivalence({ a: 1, b: 2 }, { a: 2, b: 2 }))
assertFalse(equivalence({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 }))
assertFalse(equivalence({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }))
})
it("Record(String, UndefinedOr(Number))", () => {
const schema = Schema.Record(Schema.String, Schema.UndefinedOr(Schema.Number))
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: 1, b: undefined }, { a: 1, b: undefined }))
assertFalse(equivalence({ a: 1, b: undefined }, { a: 1 }))
assertFalse(equivalence({ a: 1 }, { a: 1, b: undefined }))
})
it("Record(String.check, Number) should use the key checks to select keys", () => {
const schema = Schema.Record(Schema.String.check(Schema.isPattern(/^a/)), Schema.Number)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: 1, b: 1 }, { a: 1, b: 2 }))
assertFalse(equivalence({ a: 1 }, { a: 2 }))
})
it("Record(Symbol, Number)", () => {
const a = Symbol.for("a")
const b = Symbol.for("b")
const c = Symbol.for("c")
const schema = Schema.Record(Schema.Symbol, Schema.Number)
const equivalence = Schema.toEquivalence(schema)
assertTrue(
equivalence({ [a]: 1, [b]: 2 }, { [a]: 1, [b]: 2 })
)
assertFalse(
equivalence({ [a]: 1, [b]: 2 }, { [a]: 1, [b]: 3 })
)
assertFalse(
equivalence({ [a]: 1, [b]: 2 }, { [a]: 2, [b]: 2 })
)
assertFalse(
equivalence({ [a]: 1, [b]: 2 }, {
[a]: 1,
[b]: 2,
[c]: 3
})
)
assertFalse(
equivalence({ [a]: 1, [b]: 2, [c]: 3 }, {
[a]: 1,
[b]: 2
})
)
})
})
describe("suspend", () => {
it("recursive schema", () => {
interface A {
readonly a: string
readonly as: ReadonlyArray<A>
}
const schema = Schema.Struct({
a: Schema.String,
as: Schema.Array(Schema.suspend((): Schema.Codec<A> => schema))
})
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence({ a: "a", as: [] }, { a: "a", as: [] }))
assertFalse(equivalence({ a: "a", as: [] }, { a: "b", as: [] }))
assertFalse(equivalence({ a: "a", as: [{ a: "a", as: [] }] }, { a: "a", as: [] }))
assertFalse(equivalence({ a: "a", as: [] }, { a: "a", as: [{ a: "a", as: [] }] }))
})
it("mutually recursive schemas", () => {
interface Expression {
readonly type: "expression"
readonly value: number | Operation
}
interface Operation {
readonly type: "operation"
readonly operator: "+" | "-"
readonly left: Expression
readonly right: Expression
}
const Expression = Schema.Struct({
type: Schema.Literal("expression"),
value: Schema.Union([Schema.Finite, Schema.suspend((): Schema.Codec<Operation> => Operation)])
})
const Operation = Schema.Struct({
type: Schema.Literal("operation"),
operator: Schema.Literals(["+", "-"]),
left: Expression,
right: Expression
})
const schema = Operation
const equivalence = Schema.toEquivalence(schema)
assertTrue(
equivalence({
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
}, {
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
})
)
assertFalse(
equivalence({
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
}, {
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 3 }
})
)
assertFalse(
equivalence({
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
}, {
type: "operation",
operator: "-",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
})
)
assertFalse(
equivalence({
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
}, {
type: "operation",
operator: "+",
left: { type: "expression", value: 2 },
right: { type: "expression", value: 2 }
})
)
})
})
it("Date", () => {
const schema = Schema.Date
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(new Date(0), new Date(0)))
assertFalse(equivalence(new Date(0), new Date(1)))
})
it("URL", () => {
const schema = Schema.URL
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(new URL("https://example.com"), new URL("https://example.com")))
assertFalse(equivalence(new URL("https://example.com"), new URL("https://example.org")))
})
it("RegExp", () => {
const schema = Schema.RegExp
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(new RegExp("a"), new RegExp("a")))
assertTrue(equivalence(new RegExp("a", "i"), new RegExp("a", "i")))
assertFalse(equivalence(new RegExp("a"), new RegExp("b")))
assertFalse(equivalence(new RegExp("a", "i"), new RegExp("a", "g")))
})
it("Redacted(String)", () => {
const schema = Schema.Redacted(Schema.String)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(Redacted.make("a"), Redacted.make("a")))
assertFalse(equivalence(Redacted.make("a"), Redacted.make("b")))
})
it("Option(Modulo2)", () => {
const schema = Schema.Option(Modulo2)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(Option.none(), Option.none()))
assertTrue(equivalence(Option.some(0), Option.some(2)))
assertTrue(equivalence(Option.some(1), Option.some(3)))
assertFalse(equivalence(Option.none(), Option.some(0)))
assertFalse(equivalence(Option.some(0), Option.none()))
assertFalse(equivalence(Option.some(0), Option.some(1)))
})
it("Result(Modulo2, Modulo3)", () => {
const schema = Schema.Result(Modulo2, Modulo3)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(Result.succeed(0), Result.succeed(2)))
assertTrue(equivalence(Result.succeed(1), Result.succeed(3)))
assertTrue(equivalence(Result.fail(0), Result.fail(3)))
assertTrue(equivalence(Result.fail(1), Result.fail(4)))
assertTrue(equivalence(Result.fail(2), Result.fail(5)))
assertFalse(equivalence(Result.succeed(0), Result.fail(2)))
assertFalse(equivalence(Result.fail(0), Result.succeed(3)))
})
it("ReadonlySet(Modulo2)", () => {
const schema = Schema.ReadonlySet(Modulo2)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(new Set(), new Set()))
assertTrue(equivalence(new Set([0]), new Set([0])))
assertTrue(equivalence(new Set([0]), new Set([2])))
assertTrue(equivalence(new Set([0, 1]), new Set([1, 0])))
assertTrue(equivalence(new Set([0, 1]), new Set([2, 3])))
assertFalse(equivalence(new Set([0]), new Set([1])))
assertFalse(equivalence(new Set([0, 1]), new Set([2, 2])))
})
it("ReadonlyMap(Modulo2, Modulo3)", () => {
const schema = Schema.ReadonlyMap(Modulo2, Modulo3)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(new Map(), new Map()))
assertTrue(equivalence(new Map([[0, 1]]), new Map([[0, 1]])))
assertTrue(equivalence(new Map([[0, 1]]), new Map([[2, 4]])))
assertTrue(equivalence(new Map([[0, 1], [1, 2]]), new Map([[0, 1], [1, 2]])))
assertTrue(equivalence(new Map([[0, 1], [1, 2]]), new Map([[1, 2], [0, 1]])))
assertFalse(equivalence(new Map([[0, 1]]), new Map([[1, 1]])))
assertFalse(equivalence(new Map([[0, 1]]), new Map([[0, 2]])))
assertFalse(equivalence(new Map([[0, 1], [1, 2]]), new Map([[0, 1], [1, 3]])))
assertFalse(equivalence(new Map([[0, 1], [1, 2]]), new Map([[0, 1], [2, 2]])))
})
it("HashMap(Modulo2, Modulo3)", () => {
const schema = Schema.HashMap(Modulo2, Modulo3)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(HashMap.empty(), HashMap.empty()))
assertTrue(equivalence(HashMap.make([0, 1]), HashMap.make([0, 1])))
assertTrue(equivalence(HashMap.make([0, 1]), HashMap.make([2, 4])))
assertTrue(equivalence(HashMap.make([0, 1], [1, 2]), HashMap.make([0, 1], [1, 2])))
assertTrue(equivalence(HashMap.make([0, 1], [1, 2]), HashMap.make([1, 2], [0, 1])))
assertFalse(equivalence(HashMap.make([0, 1]), HashMap.make([1, 1])))
assertFalse(equivalence(HashMap.make([0, 1]), HashMap.make([0, 2])))
assertFalse(equivalence(HashMap.make([0, 1], [1, 2]), HashMap.make([0, 1], [1, 3])))
assertFalse(equivalence(HashMap.make([0, 1], [1, 2]), HashMap.make([0, 1], [2, 2])))
})
it("Duration", () => {
const schema = Schema.Duration
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(Duration.millis(1), Duration.millis(1)))
assertFalse(equivalence(Duration.millis(1), Duration.millis(2)))
assertTrue(equivalence(Duration.nanos(1n), Duration.nanos(1n)))
assertFalse(equivalence(Duration.nanos(1n), Duration.nanos(2n)))
assertTrue(equivalence(Duration.infinity, Duration.infinity))
assertFalse(equivalence(Duration.infinity, Duration.millis(1)))
assertTrue(equivalence(Duration.negativeInfinity, Duration.negativeInfinity))
assertFalse(equivalence(Duration.negativeInfinity, Duration.infinity))
})
it("BigDecimal", () => {
const schema = Schema.BigDecimal
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence(BigDecimal.fromStringUnsafe("1.5"), BigDecimal.fromStringUnsafe("1.50")))
assertFalse(equivalence(BigDecimal.fromStringUnsafe("1.5"), BigDecimal.fromStringUnsafe("2")))
assertTrue(equivalence(BigDecimal.fromStringUnsafe("0"), BigDecimal.fromStringUnsafe("0")))
})
it("DateTimeUtc", () => {
const schema = Schema.DateTimeUtc
const equivalence = Schema.toEquivalence(schema)
assertTrue(
equivalence(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"))
)
assertFalse(
equivalence(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z"), DateTime.makeUnsafe("2021-01-01T00:00:00.001Z"))
)
})
it("TimeZoneOffset", () => {
const equivalence = Schema.toEquivalence(Schema.TimeZoneOffset)
assertTrue(
equivalence(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000), DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))
)
assertFalse(
equivalence(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000), DateTime.zoneMakeOffset(4 * 60 * 60 * 1000))
)
})
it("TimeZoneNamed", () => {
const equivalence = Schema.toEquivalence(Schema.TimeZoneNamed)
assertTrue(
equivalence(DateTime.zoneMakeNamedUnsafe("Europe/London"), DateTime.zoneMakeNamedUnsafe("Europe/London"))
)
assertFalse(
equivalence(DateTime.zoneMakeNamedUnsafe("Europe/London"), DateTime.zoneMakeNamedUnsafe("America/New_York"))
)
})
it("TimeZone", () => {
const equivalence = Schema.toEquivalence(Schema.TimeZone)
assertTrue(
equivalence(DateTime.zoneMakeOffset(0), DateTime.zoneMakeOffset(0))
)
assertFalse(
equivalence(DateTime.zoneMakeOffset(0), DateTime.zoneMakeOffset(3 * 60 * 60 * 1000))
)
})
it("DateTimeZoned", () => {
const equivalence = Schema.toEquivalence(Schema.DateTimeZoned)
const z1 = DateTime.makeZonedUnsafe("2024-01-01T00:00:00.000Z", { timeZone: "Europe/London" })
const z2 = DateTime.makeZonedUnsafe("2024-01-02T00:00:00.000Z", { timeZone: "Europe/London" })
assertTrue(equivalence(z1, z1))
assertFalse(equivalence(z1, z2))
})
describe("Annotations", () => {
describe("overrideToEquivalence", () => {
it("String", () => {
const schema = Schema.String.pipe(
Schema.overrideToEquivalence(() => Equivalence.make((a, b) => a.substring(0, 1) === b.substring(0, 1)))
)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence("ab", "ac"))
})
it("String & isMinLength(1)", () => {
const schema = Schema.String.check(Schema.isMinLength(1)).pipe(
Schema.overrideToEquivalence(() => Equivalence.make((a, b) => a.substring(0, 1) === b.substring(0, 1)))
)
const equivalence = Schema.toEquivalence(schema)
assertTrue(equivalence("ab", "ac"))
})
})
})
})

View File

@@ -0,0 +1,585 @@
import { BigDecimal, DateTime, Duration, HashMap, Option, Redacted, Result, Schema } from "effect"
import { describe, it } from "vitest"
import { strictEqual } from "../utils/assert.ts"
describe("toFormatter", () => {
it("Never", () => {
const format = Schema.toFormatter(Schema.Never)
strictEqual(format(1 as never), "never")
})
it("Any", () => {
const format = Schema.toFormatter(Schema.Any)
strictEqual(format(1), "1")
strictEqual(format("a"), `"a"`)
strictEqual(format(true), "true")
strictEqual(format(false), "false")
strictEqual(format(null), "null")
strictEqual(format(undefined), "undefined")
strictEqual(format({ a: 1 }), `{"a":1}`)
strictEqual(format([1, 2, 3]), `[1,2,3]`)
})
it("Unknown", () => {
const format = Schema.toFormatter(Schema.Unknown)
strictEqual(format(1), "1")
strictEqual(format("a"), `"a"`)
strictEqual(format(true), "true")
strictEqual(format(false), "false")
strictEqual(format(null), "null")
strictEqual(format(undefined), "undefined")
strictEqual(format({ a: 1 }), `{"a":1}`)
strictEqual(format([1, 2, 3]), `[1,2,3]`)
})
it("Void", () => {
const format = Schema.toFormatter(Schema.Void)
strictEqual(format(undefined), "void")
})
it("Null", () => {
const format = Schema.toFormatter(Schema.Null)
strictEqual(format(null), "null")
})
it("String", () => {
const format = Schema.toFormatter(Schema.String)
strictEqual(format("a"), `"a"`)
})
it("Number", () => {
const format = Schema.toFormatter(Schema.Number)
strictEqual(format(1), "1")
})
it("Boolean", () => {
const format = Schema.toFormatter(Schema.Boolean)
strictEqual(format(true), "true")
strictEqual(format(false), "false")
})
it("BigInt", () => {
const format = Schema.toFormatter(Schema.BigInt)
strictEqual(format(1n), "1n")
})
it("Symbol", () => {
const format = Schema.toFormatter(Schema.Symbol)
strictEqual(format(Symbol.for("a")), "Symbol(a)")
})
it("UniqueSymbol", () => {
const format = Schema.toFormatter(Schema.UniqueSymbol(Symbol.for("a")))
strictEqual(format(Symbol.for("a")), "Symbol(a)")
})
it("ObjectKeyword", () => {
const format = Schema.toFormatter(Schema.ObjectKeyword)
strictEqual(format({}), "{}")
strictEqual(format({ a: 1 }), `{"a":1}`)
strictEqual(format([1, 2, 3]), `[1,2,3]`)
})
describe("Literal", () => {
it("string", () => {
const format = Schema.toFormatter(Schema.Literal("a"))
strictEqual(format("a"), `"a"`)
})
it("number", () => {
const format = Schema.toFormatter(Schema.Literal(1))
strictEqual(format(1), "1")
})
it("boolean", () => {
const format = Schema.toFormatter(Schema.Literal(true))
strictEqual(format(true), "true")
})
it("bigint", () => {
const format = Schema.toFormatter(Schema.Literal(1n))
strictEqual(format(1n), "1n")
})
})
it("Literals", () => {
const format = Schema.toFormatter(Schema.Literals(["a", "b", "c"]))
strictEqual(format("a"), `"a"`)
strictEqual(format("b"), `"b"`)
strictEqual(format("c"), `"c"`)
})
it("TemplateLiteral", () => {
const format = Schema.toFormatter(Schema.TemplateLiteral([Schema.Literal("a"), Schema.String]))
strictEqual(format("a"), `"a"`)
strictEqual(format("ab"), `"ab"`)
})
describe("Enum", () => {
it("Numeric enum", () => {
enum Fruits {
Apple,
Banana
}
const format = Schema.toFormatter(Schema.Enum(Fruits))
strictEqual(format(Fruits.Apple), "0")
})
it("String enum", () => {
enum Fruits {
Apple = "apple",
Banana = "banana",
Cantaloupe = 0
}
const format = Schema.toFormatter(Schema.Enum(Fruits))
strictEqual(format(Fruits.Apple), `"apple"`)
})
it("Const enum", () => {
const Fruits = {
Apple: "apple",
Banana: "banana",
Cantaloupe: 3
} as const
const format = Schema.toFormatter(Schema.Enum(Fruits))
strictEqual(format(Fruits.Apple), `"apple"`)
})
})
it("Union", () => {
const format = Schema.toFormatter(Schema.Union([Schema.String, Schema.Number]))
strictEqual(format("a"), `"a"`)
strictEqual(format(1), "1")
})
describe("Tuple", () => {
it("empty", () => {
const format = Schema.toFormatter(Schema.Tuple([]))
strictEqual(format([]), "[]")
})
it("elements", () => {
const format = Schema.toFormatter(Schema.Tuple([Schema.Option(Schema.String)]))
strictEqual(format([Option.some("a")]), `[some("a")]`)
strictEqual(format([Option.none()]), `[none()]`)
})
})
it("Array", () => {
const format = Schema.toFormatter(Schema.Array(Schema.Option(Schema.String)))
strictEqual(format([Option.some("a")]), `[some("a")]`)
strictEqual(format([Option.none()]), `[none()]`)
})
it("TupleWithRest", () => {
const format = Schema.toFormatter(
Schema.TupleWithRest(Schema.Tuple([Schema.Option(Schema.Boolean)]), [
Schema.Option(Schema.Number),
Schema.Option(Schema.String)
])
)
strictEqual(format([Option.some(true), Option.some(1), Option.some("a")]), `[some(true), some(1), some("a")]`)
strictEqual(format([Option.none(), Option.none(), Option.some("a")]), `[none(), none(), some("a")]`)
})
it("TupleWithRest with multiple post-rest elements", () => {
const format = Schema.toFormatter(
Schema.TupleWithRest(Schema.Tuple([Schema.String]), [
Schema.String,
Schema.Number,
Schema.Boolean,
Schema.String
])
)
strictEqual(format(["head", "tail", 1, true, "last"]), `["head", "tail", 1, true, "last"]`)
})
describe("Struct", () => {
it("empty", () => {
const format = Schema.toFormatter(Schema.Struct({}))
strictEqual(format({}), "{}")
strictEqual(format(1), "1")
strictEqual(format("a"), `"a"`)
strictEqual(format(true), "true")
strictEqual(format(false), "false")
strictEqual(format({ a: 1 }), `{"a":1}`)
strictEqual(format([1, 2, 3]), `[1,2,3]`)
})
it("required fields", () => {
const format = Schema.toFormatter(Schema.Struct({
a: Schema.Option(Schema.String)
}))
strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
})
it("required field with undefined", () => {
const format = Schema.toFormatter(Schema.Struct({
a: Schema.Option(Schema.UndefinedOr(Schema.String))
}))
strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
strictEqual(format({ a: Option.some(undefined) }), `{ "a": some(undefined) }`)
strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
})
it("optionalKey field", () => {
const format = Schema.toFormatter(Schema.Struct({
a: Schema.optionalKey(Schema.Option(Schema.String))
}))
strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
strictEqual(format({}), `{}`)
})
it("optional field", () => {
const format = Schema.toFormatter(Schema.Struct({
a: Schema.optional(Schema.Option(Schema.String))
}))
strictEqual(format({ a: Option.some("a") }), `{ "a": some("a") }`)
strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
strictEqual(format({ a: undefined }), `{ "a": undefined }`)
strictEqual(format({}), `{}`)
})
})
describe("Record", () => {
it("Record(String, Option(Number))", () => {
const format = Schema.toFormatter(Schema.Record(Schema.String, Schema.Option(Schema.Number)))
strictEqual(format({ a: Option.some(1) }), `{ "a": some(1) }`)
strictEqual(format({ a: Option.none() }), `{ "a": none() }`)
})
it("Record(String.check, Option(Number)) should use the key checks to select keys", () => {
const format = Schema.toFormatter(Schema.Record(
Schema.String.check(Schema.isPattern(/^a/)),
Schema.Option(Schema.Number)
))
strictEqual(format({ a: Option.some(1), b: Option.some(2) }), `{ "a": some(1) }`)
})
it("Record(Symbol, Option(Number))", () => {
const format = Schema.toFormatter(Schema.Record(Schema.Symbol, Schema.Option(Schema.Number)))
strictEqual(format({ [Symbol.for("a")]: Option.some(1) }), `{ Symbol(a): some(1) }`)
strictEqual(format({ [Symbol.for("a")]: Option.none() }), `{ Symbol(a): none() }`)
})
})
it("StructWithRest", () => {
const format = Schema.toFormatter(Schema.StructWithRest(
Schema.Struct({ a: Schema.Number }),
[Schema.Record(Schema.String, Schema.Number)]
))
strictEqual(format({ a: 1, b: 2 }), `{ "a": 1, "b": 2 }`)
})
it("Class", () => {
class A extends Schema.Class<A>("A")({
a: Schema.Option(Schema.String)
}) {}
const format = Schema.toFormatter(A)
strictEqual(format({ a: Option.some("a") }), `A({ "a": some("a") })`)
strictEqual(format({ a: Option.none() }), `A({ "a": none() })`)
})
describe("suspend", () => {
it("Tuple", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.Tuple([
Schema.Number,
Schema.NullOr(Rec)
])
const format = Schema.toFormatter(schema)
strictEqual(format([1, null]), `[1, null]`)
strictEqual(format([1, [2, null]]), `[1, [2, null]]`)
})
it("Array", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema: any = Schema.Array(Schema.Union([Schema.String, Rec]))
const format = Schema.toFormatter(schema)
strictEqual(format(["a"]), `["a"]`)
})
it("Struct", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.Struct({
a: Schema.String,
as: Schema.Array(Rec)
})
const format = Schema.toFormatter(schema)
strictEqual(
format({ a: "a", as: [{ a: "b", as: [] }, { a: "c", as: [] }] }),
`{ "a": "a", "as": [{ "a": "b", "as": [] }, { "a": "c", "as": [] }] }`
)
})
it("Record", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.Record(Schema.String, Rec)
const format = Schema.toFormatter(schema)
strictEqual(format({ a: { a: { a: {} } } }), `{ "a": { "a": { "a": {} } } }`)
})
it("optional", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema: any = Schema.Struct({
a: Schema.optional(Rec)
})
const format = Schema.toFormatter(schema)
strictEqual(format({ a: "a" }), `{ "a": "a" }`)
})
it("Array + Array", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema: any = Schema.Struct({
a: Schema.Array(Rec),
b: Schema.Array(Rec)
})
const format = Schema.toFormatter(schema)
strictEqual(
format({
a: [{ a: [{ a: [], b: [] }], b: [] }],
b: [{ a: [], b: [] }]
}),
`{ "a": [{ "a": [{ "a": [], "b": [] }], "b": [] }], "b": [{ "a": [], "b": [] }] }`
)
})
it("optional + Array", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema: any = Schema.Struct({
a: Schema.optional(Rec),
b: Schema.Array(Rec)
})
const format = Schema.toFormatter(schema)
strictEqual(format({ a: "a", b: [{ a: "b", b: [] }] }), `{ "a": "a", "b": [{ "a": "b", "b": [] }] }`)
})
it("mutually suspended schemas", () => {
interface Expression {
readonly type: "expression"
readonly value: number | Operation
}
interface Operation {
readonly type: "operation"
readonly operator: "+" | "-"
readonly left: Expression
readonly right: Expression
}
const Expression = Schema.Struct({
type: Schema.Literal("expression"),
value: Schema.Union([Schema.Finite, Schema.suspend((): Schema.Codec<Operation> => Operation)])
})
const Operation = Schema.Struct({
type: Schema.Literal("operation"),
operator: Schema.Literals(["+", "-"]),
left: Expression,
right: Expression
})
const format = Schema.toFormatter(Operation)
strictEqual(
format({
type: "operation",
operator: "+",
left: { type: "expression", value: 1 },
right: { type: "expression", value: 2 }
}),
`{ "type": "operation", "operator": "+", "left": { "type": "expression", "value": 1 }, "right": { "type": "expression", "value": 2 } }`
)
})
it("Option", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.Struct({
a: Schema.String,
as: Schema.Option(Rec)
})
const format = Schema.toFormatter(schema)
strictEqual(
format({ a: "a", as: Option.some({ a: "b", as: Option.none() }) }),
`{ "a": "a", "as": some({ "a": "b", "as": none() }) }`
)
})
it("ReadonlySet", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.ReadonlySet(Rec)
const format = Schema.toFormatter(schema)
strictEqual(format(new Set()), `ReadonlySet(0) {}`)
strictEqual(format(new Set([new Set([new Set()])])), `ReadonlySet(1) { ReadonlySet(1) { ReadonlySet(0) {} } }`)
})
it("ReadonlyMap", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.ReadonlyMap(Schema.String, Rec)
const format = Schema.toFormatter(schema)
strictEqual(format(new Map()), `ReadonlyMap(0) {}`)
strictEqual(
format(new Map([["a", new Map([["b", new Map()]])]])),
`ReadonlyMap(1) { "a" => ReadonlyMap(1) { "b" => ReadonlyMap(0) {} } }`
)
})
it("HashMap", () => {
const Rec = Schema.suspend((): Schema.Codec<unknown> => schema)
const schema = Schema.HashMap(Schema.String, Rec)
const format = Schema.toFormatter(schema)
strictEqual(format(HashMap.empty()), `HashMap(0) {}`)
strictEqual(
format(HashMap.make(["a", HashMap.make(["b", HashMap.empty()])])),
`HashMap(1) { "a" => HashMap(1) { "b" => HashMap(0) {} } }`
)
})
})
it("Date", () => {
const format = Schema.toFormatter(Schema.Date)
strictEqual(format(new Date(0)), "1970-01-01T00:00:00.000Z")
})
it("URL", () => {
const format = Schema.toFormatter(Schema.URL)
strictEqual(format(new URL("https://www.example.com")), "https://www.example.com/")
})
it("RegExp", () => {
const format = Schema.toFormatter(Schema.RegExp)
strictEqual(format(/a/), `/a/`)
strictEqual(format(/a/i), `/a/i`)
})
it("Option(String)", () => {
const format = Schema.toFormatter(Schema.Option(Schema.String))
strictEqual(format(Option.some("a")), `some("a")`)
strictEqual(format(Option.none()), "none()")
})
it("Result(Number, String)", () => {
const format = Schema.toFormatter(Schema.Result(Schema.Number, Schema.String))
strictEqual(format(Result.succeed(1)), `success(1)`)
strictEqual(format(Result.fail("a")), `failure("a")`)
})
it("ReadonlyMap(String, Option(Number))", () => {
const format = Schema.toFormatter(Schema.ReadonlyMap(Schema.String, Schema.Option(Schema.Number)))
strictEqual(format(new Map([["a", Option.some(1)]])), `ReadonlyMap(1) { "a" => some(1) }`)
strictEqual(format(new Map([["a", Option.none()]])), `ReadonlyMap(1) { "a" => none() }`)
})
it("HashMap(String, Option(Number))", () => {
const format = Schema.toFormatter(Schema.HashMap(Schema.String, Schema.Option(Schema.Number)))
strictEqual(format(HashMap.make(["a", Option.some(1)])), `HashMap(1) { "a" => some(1) }`)
strictEqual(format(HashMap.make(["a", Option.none()])), `HashMap(1) { "a" => none() }`)
})
describe("Redacted", () => {
it("Redacted(String)", () => {
const format = Schema.toFormatter(Schema.Redacted(Schema.String))
strictEqual(format(Redacted.make("a")), `<redacted>`)
})
it("with label", () => {
const format = Schema.toFormatter(Schema.Redacted(Schema.String, { label: "password" }))
strictEqual(format(Redacted.make("a", { label: "password" })), `<redacted:password>`)
})
})
it("Duration", () => {
const format = Schema.toFormatter(Schema.Duration)
strictEqual(format(Duration.millis(100)), `100 millis`)
strictEqual(format(Duration.nanos(1000n)), `1000 nanos`)
strictEqual(format(Duration.infinity), "Infinity")
strictEqual(format(Duration.negativeInfinity), "-Infinity")
})
it("BigDecimal", () => {
const format = Schema.toFormatter(Schema.BigDecimal)
strictEqual(format(BigDecimal.fromStringUnsafe("123.45")), "123.45")
strictEqual(format(BigDecimal.fromStringUnsafe("-5")), "-5")
strictEqual(format(BigDecimal.fromStringUnsafe("0")), "0")
})
it("DateTimeUtc", () => {
const format = Schema.toFormatter(Schema.DateTimeUtc)
strictEqual(format(DateTime.makeUnsafe("2021-01-01T00:00:00.000Z")), "DateTime.Utc(2021-01-01T00:00:00.000Z)")
})
it("TimeZoneOffset", () => {
const format = Schema.toFormatter(Schema.TimeZoneOffset)
strictEqual(format(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)), "+03:00")
})
it("TimeZoneNamed", () => {
const format = Schema.toFormatter(Schema.TimeZoneNamed)
strictEqual(format(DateTime.zoneMakeNamedUnsafe("Europe/London")), "Europe/London")
})
it("TimeZone", () => {
const format = Schema.toFormatter(Schema.TimeZone)
strictEqual(format(DateTime.zoneMakeOffset(3 * 60 * 60 * 1000)), "+03:00")
strictEqual(format(DateTime.zoneMakeNamedUnsafe("Europe/London")), "Europe/London")
})
it("DateTimeZoned", () => {
const format = Schema.toFormatter(Schema.DateTimeZoned)
const zoned = DateTime.makeZonedUnsafe("2024-01-01T00:00:00.000Z", { timeZone: "Europe/London" })
strictEqual(format(zoned), DateTime.formatIsoZoned(zoned))
})
it("custom class", () => {
class A {
constructor(readonly a: string) {}
}
const schema = Schema.instanceOf(A)
const format = Schema.toFormatter(schema)
strictEqual(format(new A("a")), `A({"a":"a"})`)
})
it("custom class with a toString() method", () => {
class A {
constructor(readonly a: string) {}
toString() {
return `A(${this.a})`
}
}
const schema = Schema.instanceOf(A)
const format = Schema.toFormatter(schema)
strictEqual(format(new A("a")), `A(a)`)
})
describe("Annotations", () => {
describe("overrideToFormatter", () => {
it("String", () => {
const schema = Schema.String.pipe(Schema.overrideToFormatter(() => (s) => s.toUpperCase()))
const format = Schema.toFormatter(schema)
strictEqual(format("a"), "A")
})
it("String & isMinLength(1)", () => {
const schema = Schema.String.check(Schema.isMinLength(1)).pipe(
Schema.overrideToFormatter(() => (s) => s.toUpperCase())
)
const format = Schema.toFormatter(schema)
strictEqual(format("a"), "A")
})
})
})
it("should allow for ast-level overrides", () => {
const toFormatter = <S extends Schema.Constraint>(schema: S) =>
Schema.toFormatter(schema, {
onBefore: (ast) => {
if (ast._tag === "Boolean") {
return (b: boolean) => b ? "True" : "False"
}
}
})
strictEqual(toFormatter(Schema.Boolean)(true), `True`)
const schema = Schema.Tuple([Schema.String, Schema.Boolean])
strictEqual(toFormatter(schema)(["a", true]), `["a", True]`)
})
})

View File

@@ -0,0 +1,394 @@
import {
Cause,
Data,
Exit,
HashMap,
Option,
Predicate,
Record,
Result,
Schema,
SchemaTransformation,
SchemaUtils
} from "effect"
import { describe, it } from "vitest"
import { assertNone, assertSome, deepStrictEqual, strictEqual, throws } from "../utils/assert.ts"
class Value extends Schema.Class<Value, { readonly brand: unique symbol }>("Value")({
a: Schema.DateValid
}) {}
function addOne(date: Date): Date {
const time = date.getTime()
if (time === -1) {
return new Date("")
}
return new Date(time + 1)
}
function addTwo(date: Date): Date {
const time = date.getTime()
return new Date(time + 2)
}
describe("Optic generation", () => {
it("overrideToCodecIso", () => {
const schema = Schema.URL.pipe(Schema.overrideToCodecIso(Schema.String, SchemaTransformation.urlFromString))
const optic = Schema.toIso(schema)
const modify = optic.modify((s) => s + "test")
deepStrictEqual(modify(new URL("https://example.com")), new URL("https://example.com/test"))
})
describe("toIso", () => {
describe("Class", () => {
it("Class", () => {
class A extends Schema.Class<A>("A")({ value: Value }) {}
class B extends Schema.Class<B>("B")({ a: A }) {}
const schema = B
const optic = Schema.toIso(schema).key("a").key("value").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify(B.make({ a: A.make({ value: Value.make({ a: new Date(0) }) }) })),
B.make({ a: A.make({ value: Value.make({ a: new Date(1) }) }) })
)
})
})
it("toType(Class)", () => {
const schema = Schema.toType(Value)
const optic = Schema.toIso(schema).key("a")
const modify = optic.modify(addOne)
deepStrictEqual(modify(Value.make({ a: new Date(0) })), Value.make({ a: new Date(1) }))
})
it("toEncoded(Class)", () => {
const schema = Schema.toEncoded(Value)
const optic = Schema.toIso(schema).key("a")
const modify = optic.modify(addOne)
deepStrictEqual(modify({ a: new Date(0) }), { a: new Date(1) })
})
describe("brand", () => {
it("Number & isPositive", () => {
const schema = Schema.Number.check(Schema.isGreaterThan(0)).pipe(Schema.brand("isPositive"))
const optic = Schema.toIso(schema)
const modify = optic.modify((n) => schema.make(n - 1))
strictEqual(modify(schema.make(2)), 1)
throws(() => modify(schema.make(1)), "Expected a value greater than 0, got 0")
})
})
it("Tuple", () => {
const schema = Schema.Tuple([Value, Schema.optionalKey(Value)])
const optic = Schema.toIso(schema).key("0").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify([Value.make({ a: new Date(0) })]),
[Value.make({ a: new Date(1) })]
)
})
it("Array", () => {
const schema = Schema.Array(Value)
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify((as) => as.map(item.modify(addOne)))
deepStrictEqual(modify([Value.make({ a: new Date(0) })]), [Value.make({ a: new Date(1) })])
})
it("NonEmptyArray", () => {
const schema = Schema.NonEmptyArray(Value)
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify(([a, ...rest]) => [item.modify(addOne)(a), ...rest.map(item.modify(addTwo))])
deepStrictEqual(
modify([
Value.make({ a: new Date(0) }),
Value.make({ a: new Date(1) }),
Value.make({ a: new Date(2) })
]),
[
Value.make({ a: new Date(1) }),
Value.make({ a: new Date(3) }),
Value.make({ a: new Date(4) })
]
)
})
it("TupleWithRest", () => {
const schema = Schema.TupleWithRest(Schema.Tuple([Value]), [Value])
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify((
[value, ...rest]
) => [item.modify(addOne)(value), ...rest.map((r) => item.modify(addTwo)(r))])
deepStrictEqual(
modify([
Value.make({ a: new Date(0) }),
Value.make({ a: new Date(1) }),
Value.make({ a: new Date(2) })
]),
[
Value.make({ a: new Date(1) }),
Value.make({ a: new Date(3) }),
Value.make({ a: new Date(4) })
]
)
})
it("Struct", () => {
const schema = Schema.Struct({
value: Value,
optionalValue: Schema.optionalKey(Value)
})
const optic = Schema.toIso(schema).key("value").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify({
value: Value.make({ a: new Date(0) })
}),
{
value: Value.make({ a: new Date(1) })
}
)
deepStrictEqual(
modify({
value: Value.make({ a: new Date(0) }),
optionalValue: Value.make({ a: new Date(2) })
}),
{
value: Value.make({ a: new Date(1) }),
optionalValue: Value.make({ a: new Date(2) })
}
)
})
it("Record", () => {
const schema = Schema.Record(Schema.String, Value)
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify((rec) => Record.map(rec, item.modify(addOne)))
deepStrictEqual(
modify({
a: Value.make({ a: new Date(0) }),
b: Value.make({ a: new Date(1) })
}),
{
a: Value.make({ a: new Date(1) }),
b: Value.make({ a: new Date(2) })
}
)
})
it("StructWithRest", () => {
const schema = Schema.StructWithRest(
Schema.Struct({ a: Value }),
[Schema.Record(Schema.String, Value)]
)
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify(({ a, ...rest }) => ({
a: item.modify(addOne)(a),
...Record.map(rest, item.modify(addTwo))
}))
deepStrictEqual(
modify({ a: Value.make({ a: new Date(0) }), b: Value.make({ a: new Date(1) }) }),
{ a: Value.make({ a: new Date(1) }), b: Value.make({ a: new Date(3) }) }
)
})
it("Union", () => {
const schema = Schema.Union([Schema.String, Value])
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify((x) => Predicate.isString(x) ? x : item.modify(addOne)(x))
deepStrictEqual(modify("a"), "a")
deepStrictEqual(modify(Value.make({ a: new Date(0) })), Value.make({ a: new Date(1) }))
})
it("suspend", () => {
interface A {
readonly a: Value
readonly as: ReadonlyArray<A>
}
interface AIso {
readonly a: typeof Value["Iso"]
readonly as: ReadonlyArray<AIso>
}
const schema = Schema.Struct({
a: Value,
as: Schema.Array(Schema.suspend((): Schema.Optic<A, AIso> => schema))
})
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const f = ({ a, as }: AIso): AIso => ({
a: item.modify(addOne)(a),
as: as.map(f)
})
const modify = optic.modify(f)
deepStrictEqual(
modify({ a: Value.make({ a: new Date(0) }), as: [{ a: Value.make({ a: new Date(1) }), as: [] }] }),
{
a: Value.make({ a: new Date(1) }),
as: [{ a: Value.make({ a: new Date(2) }), as: [] }]
}
)
})
it("flip(schema)", () => {
const schema = Schema.flip(Value)
const optic = Schema.toIso(schema).key("a")
const modify = optic.modify(addOne)
deepStrictEqual(modify(Value.make({ a: new Date(0) })), { a: new Date(1) })
})
it("flip(flip(schema))", () => {
const schema = Schema.flip(Schema.flip(Value))
const optic = Schema.toIso(schema).key("a")
const modify = optic.modify(addOne)
deepStrictEqual(modify(Value.make({ a: new Date(0) })), Value.make({ a: new Date(1) }))
})
it("Opaque", () => {
class S extends Schema.Opaque<S>()(Schema.Struct({ a: Schema.Date })) {}
const schema = S
const optic = Schema.toIso(schema).key("a")
const modify = optic.modify(addOne)
deepStrictEqual(modify({ a: new Date(0) }), { a: new Date(1) })
})
it("Option", () => {
const schema = Schema.Option(Value)
const optic = Schema.toIso(schema).tag("Some").key("value").key("a")
const modify = optic.modify(addOne)
assertSome(
modify(Option.some(Value.make({ a: new Date(0) }))),
Value.make({ a: new Date(1) })
)
assertNone(modify(Option.none()))
})
it("Result", () => {
const schema = Schema.Result(Value, Value)
const optic = Schema.toIso(schema).tag("Success").key("success").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify(Result.succeed(Value.make({ a: new Date(0) }))),
Result.succeed(Value.make({ a: new Date(1) }))
)
})
it("CauseReason", () => {
const schema = Schema.CauseReason(Value, Schema.Defect())
const optic = Schema.toIso(schema).tag("Fail").key("error").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify(Cause.makeFailReason(Value.make({ a: new Date(0) }))),
Cause.makeFailReason(Value.make({ a: new Date(1) }))
)
})
it("Cause", () => {
const schema = Schema.Cause(Value, Value)
const optic = Schema.toIso(schema)
const failure = Schema.toIsoFocus(Schema.CauseReason(Value, Value)).tag("Fail").key("error").key("a")
const modify = optic.modify((failures) => failures.map(failure.modify(addOne)))
deepStrictEqual(
modify(Cause.fail(Value.make({ a: new Date(0) }))),
Cause.fail(Value.make({ a: new Date(1) }))
)
})
it("Error", () => {
const schema = Schema.Error()
const optic = Schema.toIso(schema)
const modify = optic.modify((e) => new Error(e.message + "!"))
deepStrictEqual(modify(new Error("a")), new Error("a!"))
})
it("Exit", () => {
const schema = Schema.Exit(Value, Schema.Error(), Schema.Defect())
const optic = Schema.toIso(schema).tag("Success").key("value").key("a")
const modify = optic.modify(addOne)
deepStrictEqual(
modify(Exit.succeed(Value.make({ a: new Date(0) }))),
Exit.succeed(Value.make({ a: new Date(1) }))
)
})
it("ReadonlySet", () => {
const schema = Schema.ReadonlySet(Value)
const optic = Schema.toIso(schema)
const item = Schema.toIsoFocus(Value).key("a")
const modify = optic.modify((as) => as.map(item.modify(addOne)))
deepStrictEqual(
modify(new Set([Value.make({ a: new Date(0) })])),
new Set([Value.make({ a: new Date(1) })])
)
})
it("ReadonlyMap", () => {
const schema = Schema.ReadonlyMap(Schema.String, Value)
const optic = Schema.toIso(schema)
const entry = Schema.toIsoFocus(Schema.Tuple([Schema.String, Value])).key("1").key("a")
const modify = optic.modify((entries) => entries.map(([key, value]) => entry.modify(addOne)([key, value])))
deepStrictEqual(
modify(new Map([["a", Value.make({ a: new Date(0) })]])),
new Map([["a", Value.make({ a: new Date(1) })]])
)
})
it("HashMap", () => {
const schema = Schema.HashMap(Schema.String, Value)
const optic = Schema.toIso(schema)
const entry = Schema.toIsoFocus(Schema.Tuple([Schema.String, Value])).key("1").key("a")
const modify = optic.modify((entries) => entries.map(([key, value]) => entry.modify(addOne)([key, value])))
deepStrictEqual(
HashMap.toEntries(modify(HashMap.make(["a", Value.make({ a: new Date(0) })]))),
HashMap.toEntries(HashMap.make(["a", Value.make({ a: new Date(1) })]))
)
})
it("getNativeClassSchema", () => {
const Props = Schema.Struct({
message: Schema.String
})
class Err extends Data.Error<typeof Props.Type> {
constructor(props: typeof Props.Type) {
super(Props.make(props))
}
}
const schema = SchemaUtils.getNativeClassSchema(Err, { encoding: Props })
const optic = Schema.toIso(schema)
const modify = optic.modify((e) => new Err({ message: e.message + "!" }))
deepStrictEqual(modify(new Err({ message: "a" })), new Err({ message: "a!" }))
})
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
import { assertTrue, deepStrictEqual } from "@effect/vitest/utils"
import type { StandardJSONSchemaV1 } from "@standard-schema/spec"
import { Schema } from "effect"
import { describe, it } from "vitest"
function standardConvertToJSONSchemaInput(
schema: StandardJSONSchemaV1,
target?: StandardJSONSchemaV1.Target
): Record<string, unknown> {
return schema["~standard"].jsonSchema.input({
target: target ?? "draft-2020-12"
})
}
function standardConvertToJSONSchemaOutput(
schema: StandardJSONSchemaV1,
target?: StandardJSONSchemaV1.Target
): Record<string, unknown> {
return schema["~standard"].jsonSchema.output({
target: target ?? "draft-2020-12"
})
}
describe("toStandardJSONSchemaV1", () => {
it("should return a schema with Standard JSON Schema metadata", () => {
const schema = Schema.FiniteFromString
const standardSchema = Schema.toStandardJSONSchemaV1(schema)
assertTrue(Schema.isSchema(standardSchema))
})
it("should support both standards", () => {
const schema = Schema.String
const both = Schema.toStandardSchemaV1(Schema.toStandardJSONSchemaV1(schema))
deepStrictEqual(standardConvertToJSONSchemaInput(both), {
"type": "string"
})
})
describe("draft-2020-12", () => {
it("should return the input JSON Schema", () => {
const schema = Schema.Tuple([Schema.FiniteFromString])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaInput(standardJSONSchema), {
"type": "array",
"prefixItems": [{ "type": "string" }],
"minItems": 1,
"maxItems": 1
})
})
it("should return the output JSON Schema", () => {
const schema = Schema.Tuple([Schema.FiniteFromString])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaOutput(standardJSONSchema), {
"type": "array",
"prefixItems": [{ "type": "number" }],
"minItems": 1,
"maxItems": 1
})
})
it("a schema with identifier", () => {
const S = Schema.String.annotate({ identifier: "id" })
const schema = Schema.Tuple([S, S])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaInput(standardJSONSchema), {
"type": "array",
"prefixItems": [
{ "$ref": "#/$defs/id" },
{ "$ref": "#/$defs/id" }
],
"minItems": 2,
"maxItems": 2,
"$defs": {
"id": {
"type": "string"
}
}
})
})
})
describe("draft-07", () => {
it("should return the input JSON Schema", () => {
const schema = Schema.Tuple([Schema.FiniteFromString])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaInput(standardJSONSchema, "draft-07"), {
"type": "array",
"items": [{ "type": "string" }],
"minItems": 1,
"maxItems": 1
})
})
it("should return the output JSON Schema", () => {
const schema = Schema.Tuple([Schema.FiniteFromString])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaOutput(standardJSONSchema, "draft-07"), {
"type": "array",
"items": [{ "type": "number" }],
"minItems": 1,
"maxItems": 1
})
})
it("a schema with identifier", () => {
const S = Schema.String.annotate({ identifier: "id" })
const schema = Schema.Tuple([S, S])
const standardJSONSchema = Schema.toStandardJSONSchemaV1(schema)
deepStrictEqual(standardConvertToJSONSchemaInput(standardJSONSchema, "draft-07"), {
"type": "array",
"items": [
{ "$ref": "#/definitions/id" },
{ "$ref": "#/definitions/id" }
],
"minItems": 2,
"maxItems": 2,
"definitions": {
"id": {
"type": "string"
}
}
})
})
})
})

View File

@@ -0,0 +1,495 @@
import { assertTrue, deepStrictEqual, strictEqual } from "@effect/vitest/utils"
import type { StandardSchemaV1 } from "@standard-schema/spec"
import { Context, Effect, Option, Predicate, Schema, SchemaGetter, SchemaIssue } from "effect"
import { describe, it } from "vitest"
function validate<I, A>(
schema: StandardSchemaV1<I, A>,
input: unknown
): StandardSchemaV1.Result<A> | Promise<StandardSchemaV1.Result<A>> {
return schema["~standard"].validate(input)
}
const isPromise = (value: unknown): value is Promise<unknown> => value instanceof Promise
const expectSuccess = <A>(result: StandardSchemaV1.Result<A>, a: A) => {
deepStrictEqual(result, { value: a })
}
const expectSyncSuccess = <I, A>(
schema: StandardSchemaV1<I, A>,
input: unknown,
a: A
) => {
const result = validate(schema, input)
if (isPromise(result)) {
throw new Error("Expected value, got promise")
} else {
expectSuccess(result, a)
}
}
const expectFailure = <A>(
result: StandardSchemaV1.Result<A>,
issues: ReadonlyArray<StandardSchemaV1.Issue> | ((issues: ReadonlyArray<StandardSchemaV1.Issue>) => void)
) => {
if (result.issues !== undefined) {
if (Predicate.isFunction(issues)) {
issues(result.issues)
} else {
deepStrictEqual(
result.issues.map((issue) => ({
message: issue.message,
path: issue.path
})),
issues
)
}
} else {
throw new Error("Expected issues, got undefined")
}
}
const expectAsyncSuccess = async <I, A>(
schema: StandardSchemaV1<I, A>,
input: unknown,
a: A
) => {
const result = validate(schema, input)
if (isPromise(result)) {
expectSuccess(await result, a)
} else {
throw new Error("Expected promise, got value")
}
}
const expectSyncFailure = <I, A>(
schema: StandardSchemaV1<I, A>,
input: unknown,
issues: ReadonlyArray<StandardSchemaV1.Issue> | ((issues: ReadonlyArray<StandardSchemaV1.Issue>) => void)
) => {
const result = validate(schema, input)
if (isPromise(result)) {
throw new Error("Expected value, got promise")
} else {
expectFailure(result, issues)
}
}
const expectAsyncFailure = async <I, A>(
schema: StandardSchemaV1<I, A>,
input: unknown,
issues: ReadonlyArray<StandardSchemaV1.Issue> | ((issues: ReadonlyArray<StandardSchemaV1.Issue>) => void)
) => {
const result = validate(schema, input)
if (isPromise(result)) {
expectFailure(await result, issues)
} else {
throw new Error("Expected promise, got value")
}
}
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 AsyncNonEmptyString = AsyncString.check(Schema.isNonEmpty())
describe("toStandardSchemaV1", () => {
it("should return a Standard Schema V1 schema", () => {
const schema = Schema.FiniteFromString
const standardSchema = Schema.toStandardSchemaV1(schema)
assertTrue(Schema.isSchema(standardSchema))
})
it("should support both standards", () => {
const schema = Schema.String
const both = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))
expectSyncSuccess(both, "a", "a")
})
it("sync decoding", () => {
const schema = Schema.NonEmptyString
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncSuccess(standardSchema, "a", "a")
expectSyncFailure(standardSchema, null, [
{
message: "Expected string, got null",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: `Expected a value with a length of at least 1, got ""`,
path: []
}
])
})
it("async decoding", async () => {
const schema = AsyncNonEmptyString
const standardSchema = Schema.toStandardSchemaV1(schema)
await expectAsyncSuccess(standardSchema, "a", "a")
expectSyncFailure(standardSchema, null, [
{
message: "Expected string, got null",
path: []
}
])
await expectAsyncFailure(standardSchema, "", [
{
message: `Expected a value with a length of at least 1, got ""`,
path: []
}
])
})
describe("missing dependencies", () => {
class MagicNumber extends Context.Service<MagicNumber, number>()("MagicNumber") {}
it("sync decoding should throw", () => {
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 standardSchema = Schema.toStandardSchemaV1(schema as any)
expectSyncFailure(standardSchema, 1, (issues) => {
strictEqual(issues.length, 1)
deepStrictEqual(issues[0].path, undefined)
assertTrue(issues[0].message.includes("Service not found: MagicNumber"))
})
})
it("async decoding should report a missing dependency", () => {
const DepString = Schema.Number.pipe(Schema.decode({
decode: SchemaGetter.onSome((n) =>
Effect.gen(function*() {
const magicNumber = yield* MagicNumber
yield* Effect.sleep("10 millis")
return Option.some(n * magicNumber)
})
),
encode: SchemaGetter.passthrough()
}))
const schema = DepString
const standardSchema = Schema.toStandardSchemaV1(schema as any)
expectSyncFailure(standardSchema, 1, (issues) => {
strictEqual(issues.length, 1)
deepStrictEqual(issues[0].path, undefined)
assertTrue(issues[0].message.includes("Service not found: MagicNumber"))
})
})
})
it("by default should return all issues", () => {
const schema = Schema.Struct({
a: Schema.NonEmptyString,
b: Schema.NonEmptyString
})
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncSuccess(standardSchema, { a: "a", b: "b" }, { a: "a", b: "b" })
expectSyncFailure(standardSchema, null, [
{
message: "Expected object, got null",
path: []
}
])
expectSyncFailure(standardSchema, { a: "a", b: "" }, [
{
message: `Expected a value with a length of at least 1, got ""`,
path: ["b"]
}
])
expectSyncFailure(standardSchema, { a: "", b: "b" }, [
{
message: `Expected a value with a length of at least 1, got ""`,
path: ["a"]
}
])
expectSyncFailure(standardSchema, { a: "", b: "" }, [
{
message: `Expected a value with a length of at least 1, got ""`,
path: ["a"]
},
{
message: `Expected a value with a length of at least 1, got ""`,
path: ["b"]
}
])
})
it("with parseOptions: { errors: 'first' } should return only the first issue", () => {
const schema = Schema.Struct({
a: Schema.NonEmptyString,
b: Schema.NonEmptyString
})
const standardSchema = Schema.toStandardSchemaV1(schema, { parseOptions: { errors: "first" } })
expectSyncFailure(standardSchema, { a: "", b: "" }, [
{
message: `Expected a value with a length of at least 1, got ""`,
path: ["a"]
}
])
})
describe("Structural checks", () => {
it("Array + isMinLength", () => {
const schema = Schema.Struct({
tags: Schema.Array(Schema.NonEmptyString).check(Schema.isMinLength(3))
})
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, { tags: ["a", ""] }, [{
message: `Expected a value with a length of at least 1, got ""`,
path: ["tags", 1]
}, {
message: `Expected a value with a length of at least 3, got ["a",""]`,
path: ["tags"]
}])
})
})
describe("should respect the `message` annotation", () => {
describe("String", () => {
it("String & annotation", () => {
const schema = Schema.String.annotate({ message: "Custom message" })
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
})
it("String & annotation & isNonEmpty", () => {
const schema = Schema.String.annotate({ message: "Custom message" }).check(Schema.isNonEmpty())
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
})
it("String & isNonEmpty & annotation", () => {
const schema = Schema.String.check(Schema.isNonEmpty()).annotate({ message: "Custom message" })
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Expected string, got null",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: "Custom message",
path: []
}
])
})
it("String & isNonEmpty(annotation)", () => {
const schema = Schema.String.check(Schema.isNonEmpty({ message: "Custom message" }))
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Expected string, got null",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: "Custom message",
path: []
}
])
})
it("String & annotation & isNonEmpty & annotation", () => {
const schema = Schema.String.annotate({ message: "Custom message" }).check(Schema.isNonEmpty()).annotate({
message: "Custom message 2"
})
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: "Custom message 2",
path: []
}
])
})
it("String & annotation & isNonEmpty(annotation)", () => {
const schema = Schema.String.annotate({ message: "Custom message" }).check(
Schema.isNonEmpty({ message: "Custom message 2" })
)
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: "Custom message 2",
path: []
}
])
})
it("String & annotation & isNonEmpty(annotation) & isMaxLength(annotation)", () => {
const schema = Schema.String.annotate({ message: "Custom message" })
.check(Schema.isNonEmpty({ message: "Custom message 2" }))
.check(Schema.isMaxLength(2, { message: "Custom message 3" }))
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
expectSyncFailure(standardSchema, "", [
{
message: "Custom message 2",
path: []
}
])
expectSyncFailure(standardSchema, "abc", [
{
message: "Custom message 3",
path: []
}
])
})
})
describe("Struct", () => {
it("messageMissingKey", () => {
const schema = Schema.Struct({
a: Schema.String.annotateKey({ messageMissingKey: "Custom message" })
})
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, {}, [
{
message: "Custom message",
path: ["a"]
}
])
})
it("messageUnexpectedKey", () => {
const schema = Schema.Struct({
a: Schema.String
}).annotate({ messageUnexpectedKey: "Custom message" })
const standardSchema = Schema.toStandardSchemaV1(schema, {
parseOptions: { onExcessProperty: "error" }
})
expectSyncFailure(standardSchema, { a: "a", b: "b" }, [
{
message: "Custom message",
path: ["b"]
}
])
})
})
describe("Tuple", () => {
it("messageMissingKey", () => {
const schema = Schema.Tuple([
Schema.String.annotateKey({ messageMissingKey: "Custom message" })
])
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, [], [
{
message: "Custom message",
path: [0]
}
])
})
it("messageUnexpectedKey", () => {
const schema = Schema.Tuple([
Schema.String
]).annotate({ messageUnexpectedKey: "Custom message" })
const standardSchema = Schema.toStandardSchemaV1(schema, {
parseOptions: { onExcessProperty: "error" }
})
expectSyncFailure(standardSchema, ["a", "b"], [
{
message: "Custom message",
path: [1]
}
])
})
})
describe("Union", () => {
it("Literals", () => {
const schema = Schema.Literals(["a", "b"]).annotate({ message: "Custom message" })
const standardSchema = Schema.toStandardSchemaV1(schema)
expectSyncFailure(standardSchema, null, [
{
message: "Custom message",
path: []
}
])
expectSyncFailure(standardSchema, "-", [
{
message: "Custom message",
path: []
}
])
})
})
})
describe("treeLeafHook & verboseCheckHook", () => {
it("String", () => {
const schema = Schema.String
const standardSchema = Schema.toStandardSchemaV1(schema, {
leafHook: SchemaIssue.defaultLeafHook
})
expectSyncFailure(standardSchema, null, [
{
message: "Expected string, got null",
path: []
}
])
})
it("NonEmptyString", () => {
const schema = Schema.NonEmptyString
const standardSchema = Schema.toStandardSchemaV1(schema, {
leafHook: SchemaIssue.defaultLeafHook
})
expectSyncFailure(standardSchema, "", [
{
message: `Expected a value with a length of at least 1, got ""`,
path: []
}
])
})
})
})

View File

@@ -0,0 +1,250 @@
import { Effect, Option, Predicate, Schema, SchemaGetter } from "effect"
import { TestSchema } from "effect/testing"
import { describe, it } from "vitest"
describe("v3 -> v4 migration tests", () => {
describe("optionalWith", () => {
it("default", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.NumberFromString, { default: () => "default value" })
// })
function f<S extends Schema.Constraint>(schema: S, defaultValue: S["Type"]) {
return Schema.Struct({
a: Schema.optional(schema).pipe(
Schema.decodeTo(Schema.toType(schema), {
decode: SchemaGetter.withDefault(Effect.succeed(defaultValue)),
encode: SchemaGetter.required()
})
)
})
}
const schema = f(Schema.NumberFromString, -1)
const asserts = new TestSchema.Asserts(schema)
const decoding = asserts.decoding()
await decoding.succeed({ a: "1" }, { a: 1 })
await decoding.succeed({}, { a: -1 })
await decoding.succeed({ a: undefined }, { a: -1 })
const encoding = asserts.encoding()
await encoding.succeed({ a: 1 }, { a: "1" })
await encoding.fail(
{},
`Missing key
at ["a"]`
)
await encoding.fail(
{ a: undefined },
`Expected number, got undefined
at ["a"]`
)
})
it("default & exact", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.NumberFromString, { default: () => "default value", exact: true })
// })
function f<S extends Schema.Constraint>(schema: S, defaultValue: S["Type"]) {
return Schema.Struct({
a: Schema.optionalKey(schema).pipe(
Schema.decodeTo(Schema.toType(schema), {
decode: SchemaGetter.withDefault(Effect.succeed(defaultValue)),
encode: SchemaGetter.required()
})
)
})
}
const schema = f(Schema.NumberFromString, -1)
const asserts = new TestSchema.Asserts(schema)
const decoding = asserts.decoding()
await decoding.succeed({ a: "1" }, { a: 1 })
await decoding.succeed({}, { a: -1 })
await decoding.fail(
{ a: undefined },
`Expected string, got undefined
at ["a"]`
)
const encoding = asserts.encoding()
await encoding.succeed({ a: 1 }, { a: "1" })
await encoding.fail(
{},
`Missing key
at ["a"]`
)
await encoding.fail(
{ a: undefined },
`Expected number, got undefined
at ["a"]`
)
})
it("nullable", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.String, { nullable: true })
// })
function f<S extends Schema.Constraint>(schema: S) {
return Schema.Struct({
a: Schema.optional(Schema.NullOr(schema)).pipe(
Schema.decodeTo(Schema.optional(Schema.toType(schema)), {
decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)),
encode: SchemaGetter.passthrough()
})
)
})
}
const schema = f(Schema.NumberFromString)
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({ a: undefined })
await encoding.succeed({})
await encoding.fail(
{ a: null },
`Expected number | undefined, got null
at ["a"]`
)
})
it("nullable & exact", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, exact: true })
// })
function f<S extends Schema.Constraint>(schema: S) {
return Schema.Struct({
a: Schema.optionalKey(Schema.NullOr(schema)).pipe(
Schema.decodeTo(Schema.optionalKey(Schema.toType(schema)), {
decode: SchemaGetter.transformOptional(Option.filter(Predicate.isNotNull)),
encode: SchemaGetter.passthrough()
})
)
})
}
const schema = f(Schema.NumberFromString)
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 }, {})
await decoding.fail(
{ a: undefined },
`Expected string | null, got undefined
at ["a"]`
)
const encoding = asserts.encoding()
await encoding.succeed({ a: 1 }, { a: "1" })
await encoding.succeed({})
await encoding.fail(
{ a: undefined },
`Expected number, got undefined
at ["a"]`
)
})
it("nullable & default", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => "default value" })
// })
function f<S extends Schema.Constraint>(schema: S, defaultValue: () => S["Type"]) {
return Schema.Struct({
a: Schema.optional(Schema.NullOr(schema)).pipe(
Schema.decodeTo(Schema.UndefinedOr(Schema.toType(schema)), {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(Option.filter(Predicate.isNotNull), Option.orElseSome(defaultValue))
),
encode: SchemaGetter.required()
})
)
})
}
const schema = f(Schema.NumberFromString, () => -1)
const asserts = new TestSchema.Asserts(schema)
const decoding = asserts.decoding()
await decoding.succeed({ a: "1" }, { a: 1 })
await decoding.succeed({}, { a: -1 })
await decoding.succeed({ a: null }, { a: -1 })
await decoding.succeed({ a: undefined })
const encoding = asserts.encoding()
await encoding.succeed({ a: 1 }, { a: "1" })
await encoding.succeed({ a: undefined })
await encoding.fail(
{},
`Missing key
at ["a"]`
)
})
it("nullable & default & exact", async () => {
// const schema = Schema.Struct({
// a: Schema.optionalWith(Schema.NumberFromString, { nullable: true, default: () => "default value", exact: true })
// })
function f<S extends Schema.Constraint>(schema: S, defaultValue: () => S["Type"]) {
return Schema.Struct({
a: Schema.optionalKey(Schema.NullOr(schema)).pipe(
Schema.decodeTo(Schema.toType(schema), {
decode: SchemaGetter.transformOptional((o) =>
o.pipe(Option.filter(Predicate.isNotNull), Option.orElseSome(defaultValue))
),
encode: SchemaGetter.required()
})
)
})
}
const schema = f(Schema.NumberFromString, () => -1)
const asserts = new TestSchema.Asserts(schema)
const decoding = asserts.decoding()
await decoding.succeed({ a: "1" }, { a: 1 })
await decoding.succeed({}, { a: -1 })
await decoding.succeed({ a: null }, { a: -1 })
await decoding.fail(
{ a: undefined },
`Expected string | null, got undefined
at ["a"]`
)
const encoding = asserts.encoding()
await encoding.succeed({ a: 1 }, { a: "1" })
await encoding.fail(
{ a: undefined },
`Expected number, got undefined
at ["a"]`
)
await encoding.fail(
{},
`Missing key
at ["a"]`
)
})
})
})