Merge commit '3c60637c1a27da8ba66888de518d58d5707801f2' as 'repos/effect-smol'
This commit is contained in:
72
repos/effect-smol/packages/effect/test/schema/HMR.test.ts
Normal file
72
repos/effect-smol/packages/effect/test/schema/HMR.test.ts
Normal 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"})`)
|
||||
})
|
||||
})
|
||||
9863
repos/effect-smol/packages/effect/test/schema/Schema.test.ts
Normal file
9863
repos/effect-smol/packages/effect/test/schema/Schema.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
439
repos/effect-smol/packages/effect/test/schema/SchemaAST.test.ts
Normal file
439
repos/effect-smol/packages/effect/test/schema/SchemaAST.test.ts
Normal 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")
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)))
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Schema } from "effect"
|
||||
|
||||
export class A extends Schema.Class<A>("Class")({
|
||||
a: Schema.String
|
||||
}) {}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class A {
|
||||
constructor(public readonly a: string) {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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))`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
1708
repos/effect-smol/packages/effect/test/schema/toArbitrary.test.ts
Normal file
1708
repos/effect-smol/packages/effect/test/schema/toArbitrary.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
3193
repos/effect-smol/packages/effect/test/schema/toCodec.test.ts
Normal file
3193
repos/effect-smol/packages/effect/test/schema/toCodec.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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]`)
|
||||
})
|
||||
})
|
||||
394
repos/effect-smol/packages/effect/test/schema/toIso.test.ts
Normal file
394
repos/effect-smol/packages/effect/test/schema/toIso.test.ts
Normal 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
@@ -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"
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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: []
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
250
repos/effect-smol/packages/effect/test/schema/v3-v4.test.ts
Normal file
250
repos/effect-smol/packages/effect/test/schema/v3-v4.test.ts
Normal 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"]`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user