import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import { SchemaGenerator } from "../src/schema-generator.ts"; import { asObjectSchema, getTypeFromCode } from "./utils.ts"; /** * The object formatter's callable-property branch keeps a property whose call * signature returns a wrapper (`Stream`/`Cell`/`SqliteDb`) as its `asCell` * schema instead of skipping it (mapping spec, "Functions / callables / * constructables"). That branch returns early, so it must carry the same * attribute metadata the ordinary property path attaches — an author's JSDoc * on a factory-typed verb property is as real as one on data, and losing it * there is the #5637 prose-loss family one branch over. */ describe("object-formatter", () => { async function schemaFor(code: string) { const { type, checker, typeNode } = await getTypeFromCode( code, "SchemaRoot", ); return asObjectSchema( new SchemaGenerator().generateSchema(type, checker, typeNode), ); } it("classifies generic callable members from their instantiated arguments", async () => { const schema = await schemaFor(` type Cfc = T & { readonly __ct_cfc__?: { readonly meta?: Meta; readonly of?: T } }; type WriteAuthorizedBy = Cfc; declare const h: () => Stream; declare const cellFactory: () => Cell; declare const sqliteFactory: () => SqliteDb; declare const plainFn: () => number; declare const subPattern: () => { value: string }; type H = typeof h; interface Box { value: T; n: number } type Guarded = { action: H; value: WriteAuthorizedBy }; interface SchemaRoot { handler: Box; aliasedHandler: Box; cell: Box; sqlite: Box; plain: Box; subPattern: Box; guarded: Guarded; } `); for ( const [name, kind] of [ ["handler", "stream"], ["aliasedHandler", "stream"], ["cell", "cell"], ["sqlite", "sqlite"], ] as const ) { const member = asObjectSchema(schema.properties![name]!); expect(member.properties).toEqual({ value: { asCell: [kind] }, n: { type: "number" }, }); expect(member.required).toHaveLength(2); expect(member.required).toEqual(expect.arrayContaining(["value", "n"])); } for (const name of ["plain", "subPattern"]) { const member = asObjectSchema(schema.properties![name]!); expect(member.properties).toEqual({ n: { type: "number" } }); expect(member.required).toEqual(["n"]); } const guarded = asObjectSchema(schema.properties!.guarded!); expect(guarded.properties!.action).toEqual({ asCell: ["stream"] }); expect(asObjectSchema(guarded.properties!.value!).ifc?.writeAuthorizedBy) .toEqual({ __ctWriterIdentityOf: { file: "test.ts", path: ["h"] }, }); }); it("retains collapsed writer alternatives through a generic member", async () => { const schema = await schemaFor(` type Cfc = T & { readonly __ct_cfc__?: { readonly meta?: Meta; readonly of?: T } }; type WriteAuthorizedBy = Cfc; declare const f: (event: string) => void; declare const g: typeof f; interface Box { value: T } interface SchemaRoot { value: Box | WriteAuthorizedBy>; } `); const box = asObjectSchema(schema.properties!.value!); const value = asObjectSchema(box.properties!.value!); expect(value.anyOf).toHaveLength(2); expect(value.anyOf!.map((branch) => { const alternative = asObjectSchema(branch); expect(alternative.type).toBe("string"); return alternative.ifc?.writeAuthorizedBy; })).toEqual([ { __ctWriterIdentityOf: { file: "test.ts", path: ["f"] } }, { __ctWriterIdentityOf: { file: "test.ts", path: ["g"] } }, ]); }); it("keeps all factory wrapper kinds through nested generic members", async () => { const schema = await schemaFor(` declare const h: () => Stream; declare const cellFactory: () => Cell; declare const sqliteFactory: () => SqliteDb; declare const plainFn: () => number; declare const subPattern: () => { value: string }; interface Box { inner: { value: T; n: number } } interface SchemaRoot { handler: Box; cell: Box; sqlite: Box; plain: Box; subPattern: Box; } `); for ( const [name, kind] of [["handler", "stream"], ["cell", "cell"], [ "sqlite", "sqlite", ]] as const ) { const box = asObjectSchema(schema.properties![name]!); const inner = asObjectSchema(box.properties!.inner!); expect(inner.properties).toEqual({ value: { asCell: [kind] }, n: { type: "number" }, }); expect(inner.required).toHaveLength(2); expect(inner.required).toEqual(expect.arrayContaining(["value", "n"])); } for (const name of ["plain", "subPattern"]) { const box = asObjectSchema(schema.properties![name]!); const inner = asObjectSchema(box.properties!.inner!); expect(inner.properties).toEqual({ n: { type: "number" } }); expect(inner.required).toEqual(["n"]); } }); it("emits an open object schema for the bare `object` type", async () => { // The `object` type says a value is an object and nothing about its // properties, so the formatter claims it by name and emits a schema that // accepts any properties rather than enumerating the none it has. Which // pattern in the corpus writes a bare `object` decides whether this branch // runs at all, so it is covered on some CI runs and not others. const schema = await schemaFor(` interface SchemaRoot { bag: object; } `); expect(schema.properties?.bag).toEqual({ type: "object", additionalProperties: true, }); }); it("keeps the JSDoc description on a callable stream property", async () => { const schema = await schemaFor(` interface OpenEvent { panel: string; } interface SchemaRoot { /** Opens the composer panel. */ openComposer: () => Stream; /** The board's visible title. */ title: string; } `); const properties = asObjectSchema(schema).properties as Record< string, Record | undefined >; // Control: the ordinary property path attaches the doc. expect(properties.title?.description).toBe("The board's visible title."); // The callable branch keeps the wrapper marker AND the doc. expect(properties.openComposer?.asCell).toEqual(["stream"]); expect(properties.openComposer?.description).toBe( "Opens the composer panel.", ); }); it("lowers @deprecated on a callable stream property alongside its doc", async () => { const schema = await schemaFor(` interface OpenEvent { panel: string; } interface SchemaRoot { /** * Opens the legacy composer. * @deprecated use openComposer */ openLegacy: () => Stream; } `); const properties = asObjectSchema(schema).properties as Record< string, Record | undefined >; expect(properties.openLegacy?.asCell).toEqual(["stream"]); expect(properties.openLegacy?.deprecated).toBe(true); expect(properties.openLegacy?.description).toBe( "Opens the legacy composer.", ); }); describe("a member of an object literal read from its type", () => { // A callback's inferred result is read so. Its member is read at the // annotation of the binding it holds, where every `typeof` that annotation // names is a `PolicyOf`'s rules. const SOURCE = ` type Cfc = T & { readonly __ct_cfc__?: { readonly meta?: Meta; readonly of?: T } }; type Confidential = Cfc; type WriteAuthorizedBy = Cfc; type PolicyOf = { readonly __ct_cfc_policy_of__?: Binding }; declare const rules: { name: string }; declare function save(): void; declare const labelled: Confidential]>; declare const written: WriteAuthorizedBy; const result = { labelled, parenthesized: (labelled), written }; type SchemaRoot = typeof result; `; const memberSchemas = async () => { const { type, checker } = await getTypeFromCode(SOURCE, "SchemaRoot"); return asObjectSchema( new SchemaGenerator().generateSchema(type, checker), ).properties as Record }>; }; const POLICY = { confidentiality: [{ policyRefKind: "module", __ctPolicyIdentityOf: { path: ["rules"] }, }], }; it("reads the policy a binding's annotation names", async () => { expect((await memberSchemas()).labelled?.ifc).toMatchObject(POLICY); }); it("reads the policy of a binding read inside parentheses", async () => { expect((await memberSchemas()).parenthesized?.ifc).toMatchObject(POLICY); }); it("reads a binding whose annotation names a writer from its type", async () => { expect((await memberSchemas()).written?.ifc?.writeAuthorizedBy) .toBeUndefined(); }); }); });