From 4322e4ccc69723442b88f6908162e81168097bbd Mon Sep 17 00:00:00 2001 From: atinseau Date: Mon, 16 Mar 2026 10:43:17 +0100 Subject: [PATCH] feat(analyzer): emit minItems and maxItems for literal array templates --- AGENTS.md | 9 +- src/utils.ts | 4 + tests/array-bounds.spec.ts | 466 +++++++++++++++++++++++++++++ tests/engine.spec.ts | 61 +++- tests/root-token.spec.ts | 4 + tests/schema-type-coercion.spec.ts | 28 ++ 6 files changed, 568 insertions(+), 4 deletions(-) create mode 100644 tests/array-bounds.spec.ts diff --git a/AGENTS.md b/AGENTS.md index d153c66..7db20ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ src/ ├── schema-resolver.ts # JSON Schema navigation: $ref, combinators, additionalProperties, array items ├── errors.ts # Error hierarchy: TemplateError → Parse/Analysis/Runtime/UnsupportedSchema ├── types.ts # All public types, interfaces, diagnostic codes, defineHelper() -├── utils.ts # Shared utilities: deepEqual, LRUCache, aggregateObjectAnalysis, aggregateArrayAnalysis +├── utils.ts # Shared utilities: deepEqual, LRUCache, aggregateObjectAnalysis, aggregateArrayAnalysis (with minItems/maxItems) └── helpers/ ├── index.ts # Re-exports for helper modules ├── helper-factory.ts # Abstract HelperFactory base class + HelperRegistry interface @@ -59,18 +59,24 @@ src/ tests/ ├── fixtures.ts # Shared userSchema + userData used across test suites ├── analyzer.spec.ts # Static analysis tests (output inference, diagnostics) +├── array-bounds.spec.ts # Array minItems/maxItems output schema tests ├── parser.spec.ts # Parsing and AST tests ├── executor.spec.ts # Execution tests ├── engine.spec.ts # Typebars class integration tests ├── schema-resolver.spec.ts # Schema resolution tests ($ref, combinators) ├── math-helpers.spec.ts # Math helper tests ├── logical-helpers.spec.ts # Logical helper tests +├── map-helpers.spec.ts # Map helper tests ({{ map collection "prop" }}) ├── sub-expression.spec.ts # Sub-expression tests ├── conditional-schema.spec.ts # Conditional schema handling tests ├── colon-syntax.spec.ts # Template identifier {{key:N}} tests ├── template-identifiers.spec.ts ├── schema-type-coercion.spec.ts # coerceSchema output type coercion tests +├── coerce-execution.spec.ts # coerceSchema execution-time tests +├── exclude-template-expression.spec.ts # excludeTemplateExpression option tests +├── root-token.spec.ts # $root token tests ├── edge-cases.spec.ts # Edge case tests +├── utils.spec.ts # Utility function unit tests (deepEqual, LRUCache…) ├── migration-interop.spec.ts # Migration / interoperability tests └── integration/ # Post-build import tests (ESM + CJS) ``` @@ -222,6 +228,7 @@ bun biome check --write --unsafe src/analyzer.ts 6. **Template identifiers `{{key:N}}`** — identifier is always on the last path segment. Parsed by `extractExpressionIdentifier()`. 7. **`AnalyzeOptions` object** — `analyze()`, `validate()`, and `analyzeAndExecute()` accept an optional `AnalyzeOptions` object (not positional args) with `identifierSchemas?` and `coerceSchema?`. The `inputSchema` describes available variables for validation; it **never** influences output type coercion. 8. **`coerceSchema` for output type coercion** — by default, static literal values (`"123"`, `"true"`, `"null"`) are auto-detected by `detectLiteralType`. An explicit `coerceSchema` in `AnalyzeOptions` overrides this for static content only. Handlebars expressions, mixed templates, and JS primitive literals are never affected by `coerceSchema`. For object templates, `coerceSchema` is resolved per-property via `resolveSchemaPath()` and propagated recursively to children. +9. **Array bounds (`minItems`/`maxItems`)** — `aggregateArrayAnalysis` and `aggregateArrayAnalysisAndExecution` always emit `minItems: length, maxItems: length` in the output schema, because literal template arrays (`["{{name}}", "static"]`) have a statically-known element count. This applies to both the normal and `excludeTemplateExpression` paths (where `length` reflects the filtered count). Data-dependent arrays (e.g. `{{ map }}` helper, `{{tags}}` expression) do **not** have bounds — their size depends on runtime input. ## Validation Checklist diff --git a/src/utils.ts b/src/utils.ts index c6552ba..9d625d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -385,6 +385,8 @@ export function aggregateArrayAnalysis( outputSchema: { type: "array", items: computeArrayItemsSchema(elementSchemas), + minItems: length, + maxItems: length, }, }; } @@ -421,6 +423,8 @@ export function aggregateArrayAnalysisAndExecution( outputSchema: { type: "array", items: computeArrayItemsSchema(elementSchemas), + minItems: length, + maxItems: length, }, }; diff --git a/tests/array-bounds.spec.ts b/tests/array-bounds.spec.ts new file mode 100644 index 0000000..f7e9ef5 --- /dev/null +++ b/tests/array-bounds.spec.ts @@ -0,0 +1,466 @@ +import { describe, expect, test } from "bun:test"; +import type { JSONSchema7 } from "json-schema"; +import { analyze } from "../src/analyzer.ts"; +import { Typebars } from "../src/typebars.ts"; +import { userData, userSchema } from "./fixtures.ts"; + +// ─── Array Bounds (minItems / maxItems) Tests ──────────────────────────────── +// Verifies that `aggregateArrayAnalysis` and `aggregateArrayAnalysisAndExecution` +// correctly emit `minItems` and `maxItems` in the output schema, reflecting the +// statically-known element count of literal template arrays. + +const engine = new Typebars(); + +describe("array bounds (minItems / maxItems)", () => { + // ─── Basic cases ───────────────────────────────────────────────────────── + + describe("basic cases", () => { + test("empty array → minItems: 0, maxItems: 0", () => { + const result = engine.analyze([], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: {}, + minItems: 0, + maxItems: 0, + }); + }); + + test("single template element → minItems: 1, maxItems: 1", () => { + const result = engine.analyze(["{{name}}"], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }); + }); + + test("three template elements → minItems: 3, maxItems: 3", () => { + const result = engine.analyze( + ["{{name}}", "{{name}}", "{{name}}"], + userSchema, + ); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 3, + maxItems: 3, + }); + }); + + test("only literal strings → minItems: 2, maxItems: 2", () => { + const result = engine.analyze(["hello", "world"], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 2, + maxItems: 2, + }); + }); + + test("mixed literals and templates → minItems: 2, maxItems: 2", () => { + const result = engine.analyze(["static", "{{name}}"], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 2, + maxItems: 2, + }); + }); + }); + + // ─── Primitive literal elements ────────────────────────────────────────── + + describe("primitive literal elements", () => { + test("array with null literal → minItems: 1, maxItems: 1", () => { + const result = engine.analyze([null], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "null" }, + minItems: 1, + maxItems: 1, + }); + }); + + test("array with false literal → minItems: 1, maxItems: 1", () => { + const result = engine.analyze([false], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "boolean" }, + minItems: 1, + maxItems: 1, + }); + }); + + test("array with integer literal → minItems: 1, maxItems: 1", () => { + const result = engine.analyze([42], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "integer" }, + minItems: 1, + maxItems: 1, + }); + }); + + test("mixed primitive literals → correct count and oneOf", () => { + const result = engine.analyze([42, true, null, "hello"], userSchema); + expect(result.valid).toBe(true); + const schema = result.outputSchema; + expect(schema).toHaveProperty("minItems", 4); + expect(schema).toHaveProperty("maxItems", 4); + }); + }); + + // ─── excludeTemplateExpression ─────────────────────────────────────────── + + describe("excludeTemplateExpression", () => { + const opts = { excludeTemplateExpression: true } as const; + + test("all templates excluded → minItems: 0, maxItems: 0", () => { + const result = engine.analyze(["{{name}}", "{{age}}"], userSchema, opts); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: {}, + minItems: 0, + maxItems: 0, + }); + }); + + test("mix of static and templates — only static kept → minItems: 1, maxItems: 1", () => { + const result = engine.analyze(["static", "{{name}}"], userSchema, opts); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }); + }); + + test("only static values — all kept → minItems: 2, maxItems: 2", () => { + const result = engine.analyze(["hello", "world"], userSchema, opts); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 2, + maxItems: 2, + }); + }); + + test("empty array with excludeTemplateExpression → minItems: 0, maxItems: 0", () => { + const result = engine.analyze([], userSchema, opts); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: {}, + minItems: 0, + maxItems: 0, + }); + }); + + test("3-element array with 2 templates excluded → minItems: 1, maxItems: 1", () => { + const result = engine.analyze( + ["{{name}}", "keep-me", "{{age}}"], + userSchema, + opts, + ); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }); + }); + }); + + // ─── Nested arrays ────────────────────────────────────────────────────── + + describe("nested arrays", () => { + test("nested arrays propagate bounds at each level", () => { + const result = engine.analyze( + [["{{name}}"], ["{{age}}", "{{active}}"]], + userSchema, + ); + expect(result.valid).toBe(true); + const schema = result.outputSchema; + + // Outer array: 2 elements + expect(schema).toHaveProperty("minItems", 2); + expect(schema).toHaveProperty("maxItems", 2); + + // Inner arrays: oneOf with 2 distinct schemas (1-elem and 2-elem) + const items = (schema as Record).items as JSONSchema7; + expect(items).toHaveProperty("oneOf"); + const branches = (items as { oneOf: JSONSchema7[] }).oneOf; + expect(branches).toHaveLength(2); + + // Each branch should have its own minItems/maxItems + const branch1 = branches[0] as JSONSchema7; + const branch2 = branches[1] as JSONSchema7; + expect(branch1).toHaveProperty("minItems", 1); + expect(branch1).toHaveProperty("maxItems", 1); + expect(branch2).toHaveProperty("minItems", 2); + expect(branch2).toHaveProperty("maxItems", 2); + }); + + test("identical nested arrays → deduplicated with bounds", () => { + const result = engine.analyze( + [["{{name}}"], ["{{address.city}}"]], + userSchema, + ); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "array", + items: { + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }, + minItems: 2, + maxItems: 2, + }); + }); + }); + + // ─── Array inside object ───────────────────────────────────────────────── + + describe("array inside object", () => { + test("array property in object template → bounds on inner array", () => { + const result = engine.analyze( + { ids: ["{{name}}", "{{address.city}}"] }, + userSchema, + ); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "object", + properties: { + ids: { + type: "array", + items: { type: "string" }, + minItems: 2, + maxItems: 2, + }, + }, + required: ["ids"], + }); + }); + + test("empty array property in object template → minItems: 0", () => { + const result = engine.analyze({ list: [] }, userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toEqual({ + type: "object", + properties: { + list: { + type: "array", + items: {}, + minItems: 0, + maxItems: 0, + }, + }, + required: ["list"], + }); + }); + }); + + // ─── analyzeAndExecute ────────────────────────────────────────────────── + + describe("analyzeAndExecute", () => { + test("simple array → bounds in analysis + correct value", () => { + const { analysis, value } = engine.analyzeAndExecute( + ["{{name}}", "{{age}}"], + userSchema, + userData, + ); + expect(analysis.valid).toBe(true); + expect(analysis.outputSchema).toHaveProperty("minItems", 2); + expect(analysis.outputSchema).toHaveProperty("maxItems", 2); + expect(value).toEqual(["Alice", 30]); + }); + + test("empty array → bounds 0 in analysis + empty value", () => { + const { analysis, value } = engine.analyzeAndExecute( + [], + userSchema, + userData, + ); + expect(analysis.valid).toBe(true); + expect(analysis.outputSchema).toHaveProperty("minItems", 0); + expect(analysis.outputSchema).toHaveProperty("maxItems", 0); + expect(value).toEqual([]); + }); + + test("nested array in object → bounds on inner array", () => { + const { analysis, value } = engine.analyzeAndExecute( + { items: ["{{name}}", "static"] }, + userSchema, + userData, + ); + expect(analysis.valid).toBe(true); + const props = ( + analysis.outputSchema as { properties: Record } + ).properties; + expect(props.items).toHaveProperty("minItems", 2); + expect(props.items).toHaveProperty("maxItems", 2); + expect(value).toEqual({ items: ["Alice", "static"] }); + }); + + test("array with literals → bounds match element count", () => { + const { analysis, value } = engine.analyzeAndExecute( + [42, true, null], + userSchema, + userData, + ); + expect(analysis.valid).toBe(true); + expect(analysis.outputSchema).toHaveProperty("minItems", 3); + expect(analysis.outputSchema).toHaveProperty("maxItems", 3); + expect(value).toEqual([42, true, null]); + }); + + test("nested arrays → bounds at each level", () => { + const { analysis, value } = engine.analyzeAndExecute( + [["{{name}}"], ["{{age}}"]], + userSchema, + userData, + ); + expect(analysis.valid).toBe(true); + // Outer: 2 elements + expect(analysis.outputSchema).toHaveProperty("minItems", 2); + expect(analysis.outputSchema).toHaveProperty("maxItems", 2); + // Inner items should also have bounds + const items = (analysis.outputSchema as Record) + .items as JSONSchema7; + if ("oneOf" in items) { + for (const branch of (items as { oneOf: JSONSchema7[] }).oneOf) { + expect(branch).toHaveProperty("minItems", 1); + expect(branch).toHaveProperty("maxItems", 1); + } + } else { + expect(items).toHaveProperty("minItems", 1); + expect(items).toHaveProperty("maxItems", 1); + } + expect(value).toEqual([["Alice"], [30]]); + }); + }); + + // ─── CompiledTemplate ─────────────────────────────────────────────────── + + describe("CompiledTemplate", () => { + test("compiled array analyze → bounds present", () => { + const tpl = engine.compile(["{{name}}", "{{age}}", "static"]); + const result = tpl.analyze(userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 3); + expect(result.outputSchema).toHaveProperty("maxItems", 3); + }); + + test("compiled empty array analyze → bounds 0", () => { + const tpl = engine.compile([]); + const result = tpl.analyze(userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 0); + expect(result.outputSchema).toHaveProperty("maxItems", 0); + }); + + test("compiled array analyzeAndExecute → bounds present", () => { + const tpl = engine.compile(["{{name}}", "{{age}}"]); + const { analysis, value } = tpl.analyzeAndExecute(userSchema, userData); + expect(analysis.valid).toBe(true); + expect(analysis.outputSchema).toHaveProperty("minItems", 2); + expect(analysis.outputSchema).toHaveProperty("maxItems", 2); + expect(value).toEqual(["Alice", 30]); + }); + + test("compiled nested array → bounds at each level", () => { + const tpl = engine.compile([["{{name}}"], ["{{age}}"]]); + const result = tpl.analyze(userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 2); + expect(result.outputSchema).toHaveProperty("maxItems", 2); + }); + }); + + // ─── Standalone analyze() ─────────────────────────────────────────────── + + describe("standalone analyze()", () => { + test("standalone analyze with array → bounds present", () => { + const result = analyze(["{{name}}", "{{age}}"], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 2); + expect(result.outputSchema).toHaveProperty("maxItems", 2); + }); + + test("standalone analyze with empty array → bounds 0", () => { + const result = analyze([], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 0); + expect(result.outputSchema).toHaveProperty("maxItems", 0); + }); + + test("standalone analyze with single element → bounds 1", () => { + const result = analyze(["{{name}}"], userSchema); + expect(result.valid).toBe(true); + expect(result.outputSchema).toHaveProperty("minItems", 1); + expect(result.outputSchema).toHaveProperty("maxItems", 1); + }); + }); + + // ─── Invalid templates still have bounds ──────────────────────────────── + + describe("invalid templates still have bounds", () => { + test("array with invalid property → valid false but bounds present", () => { + const result = engine.analyze(["{{nope}}", "{{name}}"], userSchema); + expect(result.valid).toBe(false); + expect(result.outputSchema).toHaveProperty("minItems", 2); + expect(result.outputSchema).toHaveProperty("maxItems", 2); + }); + + test("array with all invalid properties → bounds reflect element count", () => { + const result = engine.analyze( + ["{{bad1}}", "{{bad2}}", "{{bad3}}"], + userSchema, + ); + expect(result.valid).toBe(false); + expect(result.outputSchema).toHaveProperty("minItems", 3); + expect(result.outputSchema).toHaveProperty("maxItems", 3); + }); + }); + + // ─── minItems and maxItems are always equal ───────────────────────────── + + describe("minItems always equals maxItems (static structure)", () => { + test("single element", () => { + const schema = engine.analyze(["{{name}}"], userSchema).outputSchema; + expect((schema as JSONSchema7).minItems).toBe( + (schema as JSONSchema7).maxItems, + ); + }); + + test("five elements", () => { + const schema = engine.analyze( + ["a", "b", "c", "d", "e"], + userSchema, + ).outputSchema; + expect((schema as JSONSchema7).minItems).toBe(5); + expect((schema as JSONSchema7).maxItems).toBe(5); + }); + + test("zero elements", () => { + const schema = engine.analyze([], userSchema).outputSchema; + expect((schema as JSONSchema7).minItems).toBe(0); + expect((schema as JSONSchema7).maxItems).toBe(0); + }); + }); +}); diff --git a/tests/engine.spec.ts b/tests/engine.spec.ts index 3f44b4c..2b10233 100644 --- a/tests/engine.spec.ts +++ b/tests/engine.spec.ts @@ -1239,6 +1239,8 @@ describe("array template input (TemplateInputArray)", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "string" }, + minItems: 1, + maxItems: 1, }); }); @@ -1251,6 +1253,8 @@ describe("array template input (TemplateInputArray)", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "string" }, + minItems: 2, + maxItems: 2, }); }); @@ -1265,6 +1269,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }], }, + minItems: 3, + maxItems: 3, }); }); @@ -1276,6 +1282,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "integer" }, { type: "boolean" }, { type: "null" }], }, + minItems: 3, + maxItems: 3, }); }); @@ -1292,6 +1300,8 @@ describe("array template input (TemplateInputArray)", () => { { type: "null" }, ], }, + minItems: 4, + maxItems: 4, }); }); @@ -1302,6 +1312,8 @@ describe("array template input (TemplateInputArray)", () => { expect(result.outputSchema).toEqual({ type: "array", items: {}, + minItems: 0, + maxItems: 0, }); }); @@ -1338,6 +1350,8 @@ describe("array template input (TemplateInputArray)", () => { }, required: ["user", "age"], }, + minItems: 1, + maxItems: 1, }); }); @@ -1348,10 +1362,22 @@ describe("array template input (TemplateInputArray)", () => { type: "array", items: { oneOf: [ - { type: "array", items: { type: "string" } }, - { type: "array", items: { type: "number" } }, + { + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }, + { + type: "array", + items: { type: "number" }, + minItems: 1, + maxItems: 1, + }, ], }, + minItems: 2, + maxItems: 2, }); }); @@ -1363,7 +1389,14 @@ describe("array template input (TemplateInputArray)", () => { expect(result.valid).toBe(true); expect(result.outputSchema).toEqual({ type: "array", - items: { type: "array", items: { type: "string" } }, + items: { + type: "array", + items: { type: "string" }, + minItems: 1, + maxItems: 1, + }, + minItems: 2, + maxItems: 2, }); }); @@ -1373,6 +1406,8 @@ describe("array template input (TemplateInputArray)", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "string" }, + minItems: 2, + maxItems: 2, }); }); @@ -1382,6 +1417,8 @@ describe("array template input (TemplateInputArray)", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "integer" }, + minItems: 3, + maxItems: 3, }); }); }); @@ -1492,6 +1529,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "string" }, { type: "integer" }], }, + minItems: 2, + maxItems: 2, }); }); @@ -1523,6 +1562,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "string" }, { type: "number" }], }, + minItems: 2, + maxItems: 2, }); expect(value).toEqual(["Alice", 30]); }); @@ -1542,6 +1583,8 @@ describe("array template input (TemplateInputArray)", () => { expect(analysis.outputSchema).toEqual({ type: "array", items: {}, + minItems: 0, + maxItems: 0, }); }); }); @@ -1559,6 +1602,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "string" }, { type: "number" }], }, + minItems: 3, + maxItems: 3, }); expect(value).toEqual(["Alice", 30, "static"]); }); @@ -1585,6 +1630,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "integer" }, { type: "boolean" }, { type: "string" }], }, + minItems: 3, + maxItems: 3, }); expect(value).toEqual([42, true, "Alice"]); }); @@ -1608,9 +1655,13 @@ describe("array template input (TemplateInputArray)", () => { { type: "array", items: { type: "number" }, + minItems: 1, + maxItems: 1, }, ], }, + minItems: 2, + maxItems: 2, }); expect(value).toEqual([{ name: "Alice" }, [30]]); }); @@ -1635,6 +1686,8 @@ describe("array template input (TemplateInputArray)", () => { expect(analysis.outputSchema).toEqual({ type: "array", items: {}, + minItems: 0, + maxItems: 0, }); expect(value).toEqual([]); }); @@ -1649,6 +1702,8 @@ describe("array template input (TemplateInputArray)", () => { items: { oneOf: [{ type: "string" }, { type: "number" }], }, + minItems: 2, + maxItems: 2, }); }); diff --git a/tests/root-token.spec.ts b/tests/root-token.spec.ts index 8c9446c..da0fa12 100644 --- a/tests/root-token.spec.ts +++ b/tests/root-token.spec.ts @@ -473,6 +473,8 @@ describe("$root token", () => { items: { oneOf: [userSchema, { type: "string" }], }, + minItems: 2, + maxItems: 2, }); }); @@ -484,6 +486,8 @@ describe("$root token", () => { items: { oneOf: [userSchema, { type: "number" }], }, + minItems: 2, + maxItems: 2, }); }); diff --git a/tests/schema-type-coercion.spec.ts b/tests/schema-type-coercion.spec.ts index 09267c5..9a1a6c7 100644 --- a/tests/schema-type-coercion.spec.ts +++ b/tests/schema-type-coercion.spec.ts @@ -1716,6 +1716,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "3" }, ], }, + minItems: 3, + maxItems: 3, }); }); @@ -1725,6 +1727,8 @@ describe("schema-driven type coercion via coerceSchema", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "number" }, + minItems: 3, + maxItems: 3, }); }); @@ -1741,6 +1745,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "false" }, ], }, + minItems: 2, + maxItems: 2, }); }); @@ -1752,6 +1758,8 @@ describe("schema-driven type coercion via coerceSchema", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "string", const: "null" }, + minItems: 2, + maxItems: 2, }); }); @@ -1769,6 +1777,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "integer", const: 3 }, ], }, + minItems: 3, + maxItems: 3, }); }); @@ -1791,6 +1801,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "hello" }, ], }, + minItems: 4, + maxItems: 4, }); }); @@ -1871,6 +1883,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "2" }, ], }, + minItems: 2, + maxItems: 2, }); }); @@ -1883,6 +1897,8 @@ describe("schema-driven type coercion via coerceSchema", () => { expect(idsSchema).toEqual({ type: "array", items: { type: "number" }, + minItems: 2, + maxItems: 2, }); }); @@ -1930,6 +1946,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "200" }, ], }, + minItems: 2, + maxItems: 2, }); }); @@ -1941,6 +1959,8 @@ describe("schema-driven type coercion via coerceSchema", () => { expect(result.outputSchema).toEqual({ type: "array", items: { type: "number" }, + minItems: 2, + maxItems: 2, }); }); @@ -1958,6 +1978,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "null" }, ], }, + minItems: 3, + maxItems: 3, }); }); @@ -1981,6 +2003,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "500" }, ], }, + minItems: 2, + maxItems: 2, }); }); }); @@ -2006,6 +2030,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "3" }, ], }, + minItems: 3, + maxItems: 3, }); expect(value).toEqual(["1", "2", "3"]); }); @@ -2035,6 +2061,8 @@ describe("schema-driven type coercion via coerceSchema", () => { { type: "string", const: "2" }, ], }, + minItems: 2, + maxItems: 2, }); expect(value).toEqual({ ids: ["1", "2"] }); });