import { describe, expect, test } from "vitest"; import * as t from "../index"; describe("or", () => { test("converts both sides to typescript", () => { expect(t.toTypescript(t.num.or(t.str))).toEqual("number\n | string"); }); test("converts to JSON Schema", () => { expect(t.toJSONSchema("union", t.num.or(t.str).or(t.bool))).toEqual({ $schema: t.JSON_SCHEMA_VERSION, title: "union", anyOf: [ { type: "number" }, { type: "string" }, { type: "boolean" }, ], }); }); test("converts union of value types to enum JSON Schema", () => { expect(t.toJSONSchema("enum", t.value("hello").or(t.value("world")).or(t.value(1)))).toEqual({ $schema: t.JSON_SCHEMA_VERSION, title: "enum", enum: [ "hello", "world", 1 ], }); }); test("accepts either of the given checks", () => { const check = t.num.or(t.str); check.assert(5); check.assert("hi"); }); test("rejects non-matching values", () => { expect(() => { const check = t.num.or(t.str); check.assert(true); }).toThrow(); }); test("accepts any of the matching checks in a sequence", () => { const check = t.num.or(t.str).or(t.bool); check.assert(5); check.assert("hi"); check.assert(true); }); test("preserves exactness on the side that is exact", () => { const check = t.exact({ hi: t.str }).or(t.subtype({ hello: t.str })); expect(() => { check.assert({ hi: "world", extra: "uh oh", }); }).toThrow(); check.assert({ hello: "world", extra: "ok", }); }); test("preserves slicing behavior on the side that is key-tracking", () => { const check = t.subtype({ hi: t.str }).or(t.num); expect(check.slice({ hi: "world", extra: "sliced", })).toEqual({ hi: "world", }); expect(check.slice(10)).toEqual(10); }); test("slices the same union branch value that passed validation", () => { const check = t.subtype({ hi: t.str }).or(t.num); let reads = 0; const input = Object.defineProperty({}, "hi", { enumerable: true, get() { reads += 1; return reads === 1 ? "valid" : 5; }, }); expect(check.slice(input)).toEqual({ hi: "valid" }); expect(reads).toBe(1); }); }); describe("and", () => { test("converts to typescript", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); expect(t.toTypescript(check)).toEqual(`{\n hi: string,\n}\n & {\n foo: string,\n }`); }); test("converts to JSON Schema", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.optional(t.str) })); expect(t.toJSONSchema("check", check)).toEqual({ $schema: t.JSON_SCHEMA_VERSION, title: "check", allOf: [ { type: "object", required: [ "hi" ], properties: { hi: { type: "string" }, }, }, { type: "object", required: [], properties: { foo: { type: "string" }, }, }, ], }); }); test("accepts values that pass both of the checks", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); check.assert({ hi: "world", foo: "bar", }); }); test("accepts values that pass both of the checks with subtype behavior", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); check.assert({ hi: "world", foo: "bar", extra: "ok", }); }); test("accepts values that pass both of the checks with exact types", () => { const check = t.exact({ hi: t.str }).and(t.exact({ foo: t.str })); check.assert({ hi: "world", foo: "bar", }); }); test("preserves exactness if both sides are exact", () => { const check = t.exact({ hi: t.str }).and(t.exact({ foo: t.str })); expect(() => { check.assert({ hi: "world", foo: "bar", extra: "uh oh", }); }).toThrow(); }); test("does not preserve exactness if the left side is inexact", () => { const check = t.subtype({ hi: t.str }).and(t.exact({ foo: t.str })); check.assert({ hi: "world", foo: "bar", extra: "uh oh", }); }); test("does not preserve exactness if the right side is inexact", () => { const check = t.exact({ hi: t.str }).and(t.subtype({ foo: t.str })); check.assert({ hi: "world", foo: "bar", extra: "uh oh", }); }); test("preserves exactness through .or calls", () => { const check = t.exact({ hi: t.str }).and( t.exact({ foo: t.str }).or(t.subtype({ test: t.str })) ); // should pass: exact match check.assert({ hi: "world", foo: "bar", }); // should fail: two exacts and-ed together expect(() => { check.assert({ hi: "world", foo: "bar", extra: "uh oh", }); }).toThrow(); // should pass: exact and subtype and-ed together check.assert({ hi: "world", test: "bar", extra: "ok", }); }); test("preserves exactness through other .and calls", () => { const check = t.exact({ hi: t.str }).and( t.exact({ foo: t.str }).and(t.exact({ test: t.str })) ); // should pass: exact match check.assert({ hi: "world", foo: "bar", test: "test", }); // should fail: exacts and-ed together expect(() => { check.assert({ hi: "world", foo: "bar", test: "test", extra: "uh oh", }); }).toThrow(); }); test("preserves inexactness through other .and calls", () => { let check = t.exact({ hi: t.str }).and( t.exact({ foo: t.str }).and(t.subtype({ test: t.str })) ); // should pass: exacts and subtypes and-ed together check.assert({ hi: "world", foo: "bar", test: "test", extra: "uh oh", }); }); test("works with non-keyed types", () => { const check = t.num.and(t.value(5)); check.assert(5); expect(() => { check.assert(6); }).toThrow(); }); test("preserves key slicing behavior of structs", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); expect(check.slice({ hi: "world", foo: "bar", extra: "sliced", })).toEqual({ hi: "world", foo: "bar", }); }); test("preserved key slicing behavior of structs through multiple and calls", () => { const a = t.subtype({ hi: t.str }); const b = t.subtype({ foo: t.str }); const c = t.subtype({ world: t.str }); const check = a.and(b).and(c); const sliced = check.slice({ hi: "a", foo: "b", world: "c", extra: "d", }); expect(sliced).toEqual({ hi: "a", foo: "b", world: "c", }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "foo", "world" ].sort()); }); test("preserved key slicing behavior of structs with dicts", () => { const a = t.subtype({ hi: t.str }); const b = t.dict(t.str); const check = a.and(b); const sliced = check.slice({ hi: "a", foo: "b", world: "c", extra: "d", }); expect(sliced).toEqual({ hi: "a", foo: "b", world: "c", extra: "d", }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "foo", "world", "extra" ].sort()); }); test("enforces structs with dicts work", () => { const a = t.subtype({ hi: t.str }); const b = t.dict(t.str); const check = a.and(b); expect(() => { check.slice({ hi: "a", foo: "b", world: "c", extra: 1, }); }).toThrow(); }); test("slicing multiple struct-dict ands works", () => { const a = t.subtype({ hi: t.str }).and(t.dict(t.str)); const b = t.dict(t.str).and(t.subtype({ world: t.str }).and(t.dict(t.str))); const check = a.and(b); const sliced = check.slice({ hi: "1", world: "2", extra: "3", }); expect(sliced).toEqual({ hi: "1", world: "2", extra: "3", }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "world", "extra", ].sort()); }); test("checking multiple struct-dict ands works", () => { const a = t.subtype({ hi: t.str }).and(t.dict(t.str)); const b = t.dict(t.str).and(t.subtype({ world: t.str }).and(t.dict(t.str))); const check = a.and(b); const sliced = check.assert({ hi: "1", world: "2", extra: "3", }); expect(sliced).toEqual({ hi: "1", world: "2", extra: "3", }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "world", "extra", ].sort()); }); test("slicing works when bracketed with or calls", () => { const a = t.subtype({ hi: t.str }); const b = t.subtype({ world: t.str }); const c = a.and(b); const d = t.subtype({ foo: t.str }); const e = d.or(c); let sliced = e.slice({ foo: "hi" }); expect(sliced).toEqual({ foo: "hi" }); expect(Object.keys(sliced)).toEqual([ "foo" ]); sliced = e.slice({ hi: "a", world: "etc", }); expect(sliced).toEqual({ hi: "a", world: "etc" }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "world" ].sort()); }); test("fails on slices that fail exactness checking", () => { const check = t.exact({ hi: t.str }).and(t.exact({ foo: t.str })); expect(check.slice({ hi: "world", foo: "bar", })).toEqual({ hi: "world", foo: "bar", }); expect(() => { check.slice({ hi: "world", foo: "bar", extra: "explode", }); }).toThrow(); }); test("preserves slicing behavior through or calls", () => { const check = t.subtype({ hi: t.str }).and( t.subtype({ foo: t.str }).or(t.subtype({ test: t.str })) ); expect(check.slice({ hi: "world", test: "test", extra: "sliced", })).toEqual({ hi: "world", test: "test", }); }); test("rejects values that don't pass the first check", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); expect(() => { check.assert({ foo: "bar", }); }).toThrow(); }); test("rejects values that don't pass the second check", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); expect(() => { check.assert({ hi: "world", }); }).toThrow(); }); test("rejects values that pass neither of the checks", () => { const check = t.subtype({ hi: t.str }).and(t.subtype({ foo: t.str })); expect(() => { check.assert({}); }).toThrow(); }); test("rejects values that are impossible", () => { const numAndNil = t.num.and(t.nil); expect(() => { numAndNil.assert(1); }).toThrow(); const strAndDict = t.str.and(t.dict(t.str)); expect(() => { strAndDict.assert(""); }).toThrow(); }); test("array intersections project TypeScript 7 indexed element keys", () => { const check = t.array(t.subtype({ hi: t.str })).and(t.array(t.subtype({ world: t.str }))); const sliced = check.slice([{ hi: "hello", world: "world", extra: "sliced", }]); type A = { hi: string }; type B = { world: string }; const typed: Array & Array = sliced; const element: A & B = typed[0]; expect(element).toEqual({ hi: "hello", world: "world", }); expect(Object.keys(element).sort()).toEqual([ "hi", "world" ].sort()); }); test("partial merges slices work", () => { const check = t.subtype({ hi: t.str, }).and(t.partial(t.subtype({ world: t.num, }))); let sliced = check.slice({ hi: "there", }); expect(sliced).toEqual({ hi: "there" }); expect(Object.keys(sliced)).toEqual([ "hi" ]); sliced = check.slice({ hi: "there", world: 5, }); expect(sliced).toEqual({ hi: "there", world: 5, }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "world" ].sort()); }); test("merging dicts of same key works", () => { const check = t.dict(t.num).and(t.dict(t.num)); check.assert({ hi: 1, world: 2, hooray: 4, }); }); test("merging dicts of same key explodes on any value", () => { const check = t.dict(t.num).and(t.dict(t.str)); expect(() => { check.assert({ hi: 1, world: 2, hooray: 4, }); }).toThrow(); }); test("merging structs wrapped in comments works", () => { const a = t.subtype({ hi: t.num, }).comment("A hi check"); const b = t.subtype({ world: t.str, }).comment("A world check"); const check = a.and(b); const sliced = check.slice({ hi: 4, world: "test", extra: true, }); expect(sliced).toEqual({ hi: 4, world: "test", }); expect(Object.keys(sliced).sort()).toEqual([ "hi", "world" ].sort()); }); test("merging nested partials and non-partials", () => { const combat = t.value("dps").comment("damage-focused"); const chardata = t.subtype({ mainRole: combat, }); const mergedchar = t.subtype({ charData: t.partial(chardata).and(chardata), }); const sliced = mergedchar.slice({ charData: { mainRole: 'dps', extra: 'hello', }, }); expect(sliced).toEqual({ charData: { mainRole: 'dps', }, }); }); });