440 lines
17 KiB
TypeScript
440 lines
17 KiB
TypeScript
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")
|
|
)
|
|
})
|
|
})
|
|
})
|