import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import { COMMONFABRIC_TYPES } from "./commonfabric-test-types.ts"; import { emittedSchemas, parseModule } from "./transformed-ast.ts"; import { transformSource } from "./utils.ts"; /** * Compiles a pattern whose `entries` input has the type `listType` and whose * body maps over it with `expression`, and returns every emitted schema that * describes an array-method callback argument, in source order. `annotated` * selects whether the pattern's destructured parameter carries an explicit * `: Input` annotation. */ async function callbackSchemas( listType: string, annotated: boolean, expression: string, ): Promise[]> { const parameter = annotated ? "({ entries }: Input)" : "({ entries })"; const output = await transformSource( `/// import { type Cfc, type Default, pattern, UI, Writable } from "commonfabric"; interface Entry { name: string; tag: string } type EntriesValue = Entry[] | Default<[]>; type EntriesCell = Writable>; interface Entries extends Array {} interface Input { entries: ${listType}; } export default pattern(${parameter} => ({ [UI]:
{${expression}}
, })); `, { types: COMMONFABRIC_TYPES, typeCheck: true }, ); return emittedSchemas(parseModule(output)).filter((schema) => (schema.properties as Record | undefined)?.element !== undefined ); } function elementOf(schema: Record | undefined): unknown { return (schema?.properties as Record | undefined)?.element; } describe("array-method-element-schema", () => { describe("a cell whose list type wraps the array", () => { // An explicit parameter annotation keeps the declared cell type, whose // argument is the whole union or intersection. Without one the receiver has // the pattern body's view, which drops a `Default` brand and keeps a `Cfc` // label. Each case compiles both and compares them, so a reading that // handles only one of the two fails. const listTypes = { "`T[] | Default<[]>`": "Writable>", "an alias of `T[] | Default<[]>`": "Writable", "an alias of the cell": "EntriesCell", "`Default`": "Writable>", "`Cfc`": 'Writable>', "a list derived from `Array`": "Writable | Default<[]>>", }; for (const [form, listType] of Object.entries(listTypes)) { it(`emits one entry as \`element\` for ${form}, with or without a parameter annotation`, async () => { const expression = "entries.map((entry) => {entry.name})"; const annotated = await callbackSchemas(listType, true, expression); const unannotated = await callbackSchemas(listType, false, expression); expect(annotated.map(elementOf)).toEqual([{ $ref: "#/$defs/Entry" }]); expect(annotated).toEqual(unannotated); }); } const methods = { filter: 'entries.filter((entry) => entry.tag === "x")', flatMap: "entries.flatMap((entry) => [{entry.name}])", }; for (const [method, expression] of Object.entries(methods)) { it(`emits one entry as \`element\` for \`${method}()\`, with or without a parameter annotation`, async () => { const listType = "Writable>"; const annotated = await callbackSchemas(listType, true, expression); const unannotated = await callbackSchemas(listType, false, expression); expect(annotated.map(elementOf)).toEqual([{ $ref: "#/$defs/Entry" }]); expect(annotated).toEqual(unannotated); }); } for (const absent of ["undefined", "null"]) { it(`emits one entry as \`element\` for \`T[] | ${absent}\``, async () => { const schemas = await callbackSchemas( `Writable`, true, "entries.map((entry) => {entry?.name})", ); expect(schemas.map(elementOf)).toEqual([{ $ref: "#/$defs/Entry" }]); }); } }); describe("an array receiver", () => { // The receiver is the list here, so its element is emitted whole and is // never read as a list type in turn. it("emits a row as `element` for a list of lists", async () => { const schemas = await callbackSchemas( "Entry[][]", true, "entries.map((row) => {row.length})", ); expect(schemas.map(elementOf)).toEqual([{ type: "array", items: { $ref: "#/$defs/Entry" }, }]); }); it("emits the union of a tuple's positions as `element`", async () => { const schemas = await callbackSchemas( "[string, number]", true, "entries.map((position) => {String(position)})", ); expect(schemas.map(elementOf)).toEqual([{ type: ["number", "string"] }]); }); it("emits the whole element of an interface extending `Array`", async () => { const schemas = await callbackSchemas( "Entries", true, "entries.map((row) => {String(row)})", ); expect(schemas.map(elementOf)).toEqual([{ anyOf: [{ type: "number" }, { type: "array", items: { type: "string" }, }], }]); }); it("emits a row as `element` for an interface extending `Array`", async () => { const schemas = await callbackSchemas( "Entries", true, "entries.map((row) => {row.length})", ); expect(schemas.map(elementOf)).toEqual([{ type: "array", items: { $ref: "#/$defs/Entry" }, }]); }); it("emits the array's own element when that element is itself a union around an array", async () => { const schemas = await callbackSchemas( "(string[] | number)[]", true, "entries.map((row) => {String(row)})", ); expect(schemas.map(elementOf)).toEqual([{ anyOf: [{ type: "number" }, { type: "array", items: { type: "string" }, }], }]); }); }); });