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 => Equivalence.make((a, b) => a % 2 === b % 2) }) const Modulo3 = Schema.Number.annotate({ toEquivalence: (): Equivalence.Equivalence => 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 } const schema = Schema.Struct({ a: Schema.String, as: Schema.Array(Schema.suspend((): Schema.Codec => 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)]) }) 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")) }) }) }) })