import * as l from "../mod.ts"; import { assert, equal, throws } from "./assert.ts"; Deno.test("named accepts eager or memoized lazy definitions without losing descriptions", () => { let calls = 0; const Lazy = l.named("blue.cerulea.app.defs#holder", () => { calls++; return l.object({ item: Item }); }); const Described = l.withDescription(Lazy, "A holder."); const Item = l.named( "blue.cerulea.app.defs#item", l.object({ text: l.string }), ); assert(calls === 0); const Eager = l.named( "blue.cerulea.app.defs#holder", l.object({ item: Item }), ); const value = { item: { text: "hello" } }; assert(Described.validate(value).success); assert(Lazy.validate(value).success); equal(l.toLexicons(Lazy), l.toLexicons(Eager)); equal(calls, 1); equal(Described.description, "A holder."); equal(Lazy.description, undefined); const Container = l.named( "blue.cerulea.app.defs#container", l.object({ holder: Described }), ); equal(l.toLexicons(Container)[0]!.defs.container!.properties, { holder: { type: "ref", ref: Lazy.id, description: "A holder." }, }); }); Deno.test("recursive objects emit refs, validate nested paths, and share untouched builder values", () => { type Tree = { readonly text: string; readonly children?: readonly Tree[] | null; }; const Tree: l.NamedSchema<"blue.cerulea.app.defs#tree", Tree> = l.named( "blue.cerulea.app.defs#tree", () => l.object({ text: l.string, children: l.optional(l.nullable(l.array(Tree))), }), ); const Holder = l.record("blue.cerulea.app.tree", { key: "tid" }, { tree: Tree, }); const leaf: Tree = { text: "leaf", children: null }; const tree = { text: "root", children: [leaf, leaf, { text: "branch", children: [leaf] }], }; const original = structuredClone(tree); assert(Tree.validate(tree).success); assert(Holder.build({ tree }).tree === tree); assert(Holder.validate(Holder.build({ tree })).success); equal(tree, original); const bad = Tree.validate({ text: "root", children: [{ text: "branch", children: [{ text: 1 }] }], }); assert(!bad.success); equal(bad.issues.map((issue) => issue.path), [[ "children", 0, "children", 0, "text", ]]); equal(l.toLexicons(Tree), [{ lexicon: 1, id: "blue.cerulea.app.defs", defs: { tree: { type: "object", properties: { text: { type: "string" }, children: { type: "array", items: { type: "ref", ref: Tree.id } }, }, required: ["text"], nullable: ["children"], }, }, }]); equal(l.toLexicons(Holder, Tree), l.toLexicons(Holder)); }); Deno.test("unions of named definitions support recursive variants and reuse ordinary refs", () => { type Thread = { readonly text: string; readonly replies?: readonly Reply[] }; type Reply = | ({ readonly $type: "app.bsky.feed.defs#threadViewPost" } & Thread) | { readonly $type: "app.bsky.feed.defs#notFoundPost"; readonly uri: `at://${string}`; }; const Thread: l.NamedSchema<"app.bsky.feed.defs#threadViewPost", Thread> = l .named( "app.bsky.feed.defs#threadViewPost", () => l.object({ text: l.string, replies: l.optional(l.array(l.union([Thread, Missing]))), }), ); const Missing = l.named( "app.bsky.feed.defs#notFoundPost", l.object({ uri: l.atUri }), ); const Reply = l.union([Thread, Missing]); const value: l.Input = { $type: Thread.id, text: "root", replies: [{ $type: Thread.id, text: "child", replies: [{ $type: Missing.id, uri: "at://did:plc:alice/app.bsky.feed.post/abc", }], }], }; const built = Reply.build(value); equal(built, value); assert(Reply.validate(built).success); const bad = Reply.validate({ ...value, replies: [{ $type: Thread.id, text: "child", replies: [{ $type: Missing.id, uri: "bad" }], }], }); assert(!bad.success); equal(bad.issues.map((issue) => issue.path), [[ "replies", 0, "replies", 0, "uri", ]]); assert( !Reply.validate({ ...value, $type: "app.bsky.feed.defs#other" }).success, ); assert( l.union([Thread, Missing], { closed: false }).validate({ $type: "app.bsky.feed.defs#future", }).success, ); const docs = l.toLexicons(Reply, Thread, Missing); equal(docs, l.toLexicons(Missing, Thread)); equal(docs, [{ lexicon: 1, id: "app.bsky.feed.defs", defs: { notFoundPost: { type: "object", properties: { uri: { type: "string", format: "at-uri" } }, required: ["uri"], }, threadViewPost: { type: "object", properties: { text: { type: "string" }, replies: { type: "array", items: { type: "union", refs: [Missing.id, Thread.id], closed: true, }, }, }, required: ["text"], }, }, }]); }); Deno.test("builders expand unions and records throughout mutually recursive definitions", () => { const Payload = l.union("blue.cerulea.app.payload", { "#text": l.object({ text: l.string }), }); const Note = l.record("blue.cerulea.app.note", { key: "tid" }, { text: l.string, }); type A = { readonly b?: B; readonly payload: l.Infer; readonly note: l.Infer; }; type B = { readonly a?: A }; type AInput = { readonly b?: BInput; readonly payload: l.Input; readonly note: l.Input; }; type BInput = { readonly a?: AInput }; const A: l.NamedSchema<"blue.cerulea.app.a#view", A, AInput> = l.named( "blue.cerulea.app.a#view", () => l.object({ b: l.optional(B), payload: Payload, note: Note }), ); const B: l.NamedSchema<"blue.cerulea.app.b#view", B, BInput> = l.named( "blue.cerulea.app.b#view", () => l.object({ a: l.optional(A) }), ); const Holder = l.record("blue.cerulea.app.holder", { key: "tid" }, { a: A, b: B, }); const payload = { $type: "#text", text: "hello" } as const; const note = { text: "note" }; const leaf = { payload, note }; const input = { a: { b: { a: leaf }, ...leaf }, b: { a: { b: { a: leaf }, ...leaf } }, }; const original = structuredClone(input); const expanded = { payload: { $type: "blue.cerulea.app.payload#text", text: "hello" }, note: { $type: "blue.cerulea.app.note", text: "note" }, }; equal(Payload.build(payload), expanded.payload); equal(Note.build(note), expanded.note); const expected = { $type: Holder.id, a: { b: { a: expanded }, ...expanded }, b: { a: { b: { a: expanded }, ...expanded } }, }; equal(Holder.build(input), expected); equal(Holder.build(input), expected); equal(Payload.build(payload), expanded.payload); equal(Note.build(note), expanded.note); assert(Holder.validate(expected).success); equal(input, original); equal(l.toLexicons(A, B, Holder), l.toLexicons(Holder)); const docs = l.toLexicons(A); equal( docs.find((doc) => doc.id === "blue.cerulea.app.b")!.defs.view!.properties, { a: { type: "ref", ref: A.id }, }, ); }); Deno.test("lazy definitions can lead back to an enclosing record", () => { type Tree = { readonly $type: "blue.cerulea.app.tree"; readonly text: string; readonly branch?: Branch; }; type Branch = { readonly tree?: Tree }; type TreeInput = { readonly $type?: "blue.cerulea.app.tree"; readonly text: string; readonly branch?: BranchInput; }; type BranchInput = { readonly tree?: TreeInput }; const Branch: l.NamedSchema<"blue.cerulea.app.tree#branch", Branch, BranchInput> = l.named( "blue.cerulea.app.tree#branch", () => l.object({ tree: l.optional(Tree) }), ); const Tree = l.record("blue.cerulea.app.tree", { key: "tid" }, { text: l.string, branch: l.optional(Branch), }); const built = Tree.build({ text: "root", branch: { tree: { text: "leaf" } } }); equal(built, { $type: Tree.id, text: "root", branch: { tree: { $type: Tree.id, text: "leaf" } }, }); assert(Tree.validate(built).success); const [doc] = l.toLexicons(Tree); equal(Object.keys(doc!.defs), ["branch", "main"]); equal(doc!.defs.branch!.properties, { tree: { type: "ref", ref: Tree.id } }); equal(doc!.defs.main!.record, { type: "object", properties: { text: { type: "string" }, branch: { type: "ref", ref: Branch.id } }, required: ["text"], }); }); Deno.test("recursive array definitions and lazy RPC bodies retain their schemas", () => { type Nested = readonly Nested[]; const Nested: l.NamedSchema<"blue.cerulea.app.defs#nested", Nested> = l.named( "blue.cerulea.app.defs#nested", () => l.array(Nested), ); assert(Nested.validate([[], [[[]]]]).success); const bad = Nested.validate([[1]]); assert(!bad.success); equal(bad.issues.map((issue) => issue.path), [[0, 0]]); equal(l.toLexicons(Nested)[0]!.defs.nested, { type: "array", items: { type: "ref", ref: Nested.id }, }); const Body = l.named( "blue.cerulea.app.defs#body", () => l.object({ nested: Nested }), ); const endpoint = l.procedure("blue.cerulea.app.echo", { input: Body, output: Body, }); assert(endpoint.input.validate({ nested: [[]] }).success); equal( l.toLexicons(endpoint).find((doc) => doc.id === endpoint.id)!.defs.main! .output, { encoding: "application/json", schema: { type: "ref", ref: Body.id }, }, ); throws( () => l.query("blue.cerulea.app.read", { output: Nested }), "RPC bodies", ); }); Deno.test("recursive definition deduplication does not hide conflicting IDs", () => { type Link = { readonly value: T; readonly next?: Link }; const id = "blue.cerulea.app.defs#link"; const A: l.NamedSchema> = l.named( id, () => l.object({ value: l.string, next: l.optional(A) }), ); const Same: l.NamedSchema> = l.named( id, () => l.object({ value: l.string, next: l.optional(Same) }), ); const Different: l.NamedSchema> = l.named( id, () => l.object({ value: l.integer, next: l.optional(Different) }), ); equal(l.toLexicons(A, Same), l.toLexicons(A)); throws(() => l.toLexicons(A, Different), `Conflicting definition: ${id}`); throws(() => l.toLexicons(Different, A), `Conflicting definition: ${id}`); const NestedConflict: l.NamedSchema = l.named( id, () => l.object({ next: Different }), ); throws(() => l.toLexicons(NestedConflict), `Conflicting definition: ${id}`); }); Deno.test("lazy targets enforce concrete definitions and failed resolution can be retried", () => { let available = false; const Retry = l.named("blue.cerulea.app.defs#retry", () => { if (!available) throw new Error("Not ready"); return l.object({ text: l.string }); }); const Holder = l.record("blue.cerulea.app.retry", { key: "tid" }, { item: Retry, }); throws(() => Holder.build({ item: { text: "hi" } }), "Not ready"); throws(() => Retry.validate({}), "Not ready"); throws(() => l.toLexicons(Retry), "Not ready"); available = true; assert(Holder.validate(Holder.build({ item: { text: "hi" } })).success); assert(l.toLexicons(Retry).length === 1); for ( const target of [ Retry, l.union({ "blue.cerulea.app.defs#empty": l.object({}) }), Holder, ] ) { throws( () => l.named("blue.cerulea.app.defs#invalid", target), "concrete definitions", ); const Lazy = l.named("blue.cerulea.app.defs#invalid", () => target); throws(() => l.compile(Lazy), "concrete definitions"); throws(() => l.toLexicons(Lazy), "concrete definitions"); } const Reentrant: l.NamedSchema = l.named( "blue.cerulea.app.defs#reentrant", () => Reentrant.target, ); throws(() => l.toLexicons(Reentrant), "Circular definition factory"); throws(() => l.compile(Reentrant), "Circular definition factory"); throws(() => l.named("invalid", () => l.string), "definition ID"); }); Deno.test("named union variants must resolve to objects and have unique IDs", () => { const Text = l.named("blue.cerulea.app.defs#text", () => l.string); const Union = l.union([Text]); throws(() => l.compile(Union), "must be an object"); throws(() => l.toLexicons(Union), "must be an object"); throws(() => Union.build({ $type: Text.id } as never), "must be an object"); const Object = l.named("blue.cerulea.app.defs#object", l.object({})); throws(() => l.union([Object, Object]), "Duplicate union variant"); throws(() => l.union([]), "empty"); assert( l.union([], { closed: false }).validate({ $type: "blue.cerulea.app.defs#future", }).success, ); });