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 = 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 = {} 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 = {} 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")({ 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: Schema.String }) {} const ast = A.ast deepStrictEqual(SchemaAST.collectSentinels(ast), [{ key: "_tag", literal: "A" }]) }) it("ErrorClass", () => { class E extends Schema.ErrorClass("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: 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") ) }) }) })