import * as l from "../mod.ts"; import { assert, equal, throws } from "./assert.ts"; Deno.test("queries emit parameters, JSON output, errors, and referenced definitions", () => { const Actor = l.named("blue.cerulea.app.views#actor", l.object({ did: l.did })); const endpoint = l.query("blue.cerulea.app.getActors", { description: "Get actors.", parameters: l.params({ actors: l.array(l.atIdentifier, { minLength: 1, maxLength: 10 }), limit: l.optional(l.integerWith({ minimum: 1, maximum: 10 })), }), output: l.object({ actors: l.array(Actor) }), errors: [{ name: "ActorNotFound", description: "No such actor." }], }); equal(l.toLexicons(endpoint), [ { lexicon: 1, id: "blue.cerulea.app.getActors", defs: { main: { type: "query", description: "Get actors.", parameters: { type: "params", properties: { actors: { type: "array", items: { type: "string", format: "at-identifier" }, minLength: 1, maxLength: 10, }, limit: { type: "integer", minimum: 1, maximum: 10 }, }, required: ["actors"], }, output: { encoding: "application/json", schema: { type: "object", properties: { actors: { type: "array", items: { type: "ref", ref: "blue.cerulea.app.views#actor" }, }, }, required: ["actors"], }, }, errors: [{ name: "ActorNotFound", description: "No such actor." }], }, }, }, { lexicon: 1, id: "blue.cerulea.app.views", defs: { actor: { type: "object", properties: { did: { type: "string", format: "did" } }, required: ["did"], }, }, }, ]); }); Deno.test("procedures support JSON input and intentionally absent output", () => { const endpoint = l.procedure("blue.cerulea.app.setMute", { input: l.object({ actor: l.did, muted: l.boolean }), }); equal(l.toLexicons(endpoint), [{ lexicon: 1, id: "blue.cerulea.app.setMute", defs: { main: { type: "procedure", input: { encoding: "application/json", schema: { type: "object", properties: { actor: { type: "string", format: "did" }, muted: { type: "boolean" } }, required: ["actor", "muted"], }, }, }, }, }]); const check = l.compile(endpoint.input); const value = { actor: "did:plc:alice", muted: false }; const result = check(value); assert(result.success && result.value === value); assert(!check({ actor: "alice.example", muted: false }).success); assert(!check({ actor: "did:plc:alice" }).success); }); Deno.test("RPC bodies can reference records or use tagged unions", () => { const Record = l.record("blue.cerulea.app.example", { key: "tid" }, { text: l.string }); const Result = l.union("blue.cerulea.app.result", { "#success": l.object({ record: Record }), "#failure": l.object({ message: l.string }), }); const endpoint = l.procedure("blue.cerulea.app.echo", { input: Record, output: Result }); const docs = l.toLexicons(endpoint, Record); const def = docs.find((doc) => doc.id === endpoint.id)!.defs.main!; equal(def.input, { encoding: "application/json", schema: { type: "ref", ref: "blue.cerulea.app.example" }, }); equal(def.output, { encoding: "application/json", schema: { type: "union", refs: ["blue.cerulea.app.result#failure", "blue.cerulea.app.result#success"], closed: true, }, }); equal(docs.find((doc) => doc.id === "blue.cerulea.app.example")!.defs.main!.type, "record"); assert( l.compile(endpoint.output)({ $type: "blue.cerulea.app.result#failure", message: "no" }).success, ); }); Deno.test("query parameter validation checks decoded scalar values and arrays", () => { const check = l.compile(l.params({ actor: l.atIdentifier, enabled: l.optional(l.boolean), limit: l.optional(l.integerWith({ minimum: 1, maximum: 100 })), ids: l.optional(l.array(l.string, { maxLength: 2 })), })); assert(check({ actor: "alice.example", enabled: false, limit: 10, ids: ["a"] }).success); assert(check({ actor: "did:plc:alice" }).success); for ( const bad of [ {}, { actor: "alice" }, { actor: "alice.example", enabled: "false" }, { actor: "alice.example", limit: "10" }, { actor: "alice.example", limit: 0 }, { actor: "alice.example", ids: ["a", "b", "c"] }, { actor: "alice.example", enabled: null }, ] ) assert(!check(bad).success); }); Deno.test("unsupported RPC shapes and conflicting IDs fail explicitly", () => { for ( const field of [ l.object({}), l.blob(), l.bytes, l.cidLink, l.nullable(l.string), l.optional(l.nullable(l.string)), l.array(l.array(l.string)), l.named("blue.cerulea.app.defs#text", l.string), ] ) { throws(() => l.params({ field }), "Parameter field"); } throws(() => l.query("invalid", {}), "RPC NSID"); throws(() => l.procedure("blue.cerulea.app.write", { input: l.array(l.string) }), "RPC bodies"); throws( () => l.query("blue.cerulea.app.read", { output: l.named("blue.cerulea.app.defs#text", l.string) }), "RPC bodies", ); throws( () => l.query("blue.cerulea.app.read", { errors: [{ name: "Bad Error" }] }), "Invalid RPC error", ); throws( () => l.query("blue.cerulea.app.read", { errors: [{ name: "Bad" }, { name: "Bad" }] }), "Duplicate RPC error", ); throws(() => l.toLexicons( l.query("blue.cerulea.app.read", {}), l.procedure("blue.cerulea.app.read", {}), ), "Conflicting definition"); // @ts-expect-error Queries cannot have request bodies. throws(() => l.query("blue.cerulea.app.read", { input: l.object({}) }), "Queries cannot"); // @ts-expect-error Params are only used as RPC parameters, never as named definitions. throws(() => l.toLexicons(l.params({})), "roots"); }); export function rpcInference(): void { const endpoint = l.procedure("blue.cerulea.app.example", { parameters: l.params({ dryRun: l.optional(l.boolean) }), input: l.object({ name: l.string, labels: l.optional(l.array(l.string)) }), output: l.object({ accepted: l.boolean }), errors: [{ name: "Denied" }], }); const input: l.Infer = { name: "Alice" }; const output: l.Infer = { accepted: true }; const parameters: l.Infer = { dryRun: false }; const id: "blue.cerulea.app.example" = endpoint.id; const errorName: "Denied" = endpoint.errors[0].name; void [output, parameters, id, errorName]; // @ts-expect-error Input properties remain readonly. input.name = "Bob"; // @ts-expect-error Required body fields are retained. const missing: l.Infer = {}; // @ts-expect-error Parameter types are not strings just because HTTP transports them as strings. const wrong: l.Infer = { dryRun: "false" }; const undefinedValue: l.Infer = { dryRun: undefined }; // @ts-expect-error RPCs are not data validators; compile a parameters/input/output schema. l.compile(endpoint); // @ts-expect-error Params cannot appear inside stored data. l.array(endpoint.parameters); void [missing, wrong, undefinedValue]; }