diff --git a/src/JsonSchemaGen.ts b/src/JsonSchemaGen.ts index deb197e..7f8d080 100644 --- a/src/JsonSchemaGen.ts +++ b/src/JsonSchemaGen.ts @@ -15,6 +15,16 @@ const make = Effect.gen(function* () { const refStore = new Map() function cleanupSchema(schema: JsonSchema.JsonSchema) { + // Handle boolean schemas (true/false) + if (typeof schema === "boolean") { + return schema + } + + // Ensure schema is an object before using 'in' operator + if (typeof schema !== "object" || schema === null) { + return schema + } + if ( "type" in schema && Array.isArray(schema.type) && @@ -75,6 +85,17 @@ const make = Effect.gen(function* () { asStruct = true, ) { schema = cleanupSchema(schema) + + // Early return for boolean schemas + if (typeof schema === "boolean") { + return + } + + // Ensure schema is an object before property access + if (typeof schema !== "object" || schema === null) { + return + } + const enumSuffix = childName?.endsWith("Enum") ? "" : "Enum" if ("$ref" in schema) { if (seenRefs.has(schema.$ref)) { @@ -210,6 +231,23 @@ const make = Effect.gen(function* () { topLevel = false, ): Option.Option => { schema = cleanupSchema(schema) + + // Handle boolean schemas + if (typeof schema === "boolean") { + if (schema === true) { + // true = any/unknown + return Option.some(transformer.onUnknown({ importName })) + } else { + // false = never/no additional items - return empty/none + return Option.none() + } + } + + // Ensure schema is an object before property access + if (typeof schema !== "object" || schema === null) { + return Option.none() + } + if ("properties" in schema) { const obj = schema as JsonSchema.Object const required = obj.required ?? [] @@ -413,6 +451,9 @@ const make = Effect.gen(function* () { return { $id: "/schemas/any" } } else if (Array.isArray(schema)) { return { anyOf: schema } + } else if (typeof schema === "boolean") { + // Handle boolean schemas: false means no additional items, true means any item + return schema === false ? { not: {} } : { $id: "/schemas/any" } } return schema } @@ -524,6 +565,8 @@ export class JsonSchemaTransformer extends Context.Tag("JsonSchemaTransformer")< readonly source: string }> }): string + + onUnknown(options: { readonly importName: string }): string } >() {} @@ -655,6 +698,9 @@ export const layerTransformerSchema = Layer.sync(JsonSchemaTransformer, () => { onUnion({ importName, items }) { return `${importName}.Union(${items.map((_) => `${toComment(_.description)}${_.source}`).join(",\n")})` }, + onUnknown({ importName }) { + return `${importName}.Unknown` + }, }) }) @@ -716,6 +762,9 @@ export type ${name} = (typeof ${name})[keyof typeof ${name}];` } return `{\n ${items.map(({ description, title, source }) => `${toComment(description)}${JSON.stringify(Option.getOrNull(title))}: ${source}`).join(",\n ")}} as const\n` }, + onUnknown() { + return "unknown" + }, }), ) diff --git a/test-fixtures/boolean-schema-test.json b/test-fixtures/boolean-schema-test.json new file mode 100644 index 0000000..f0199e0 --- /dev/null +++ b/test-fixtures/boolean-schema-test.json @@ -0,0 +1,120 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Boolean Schema Test API", + "version": "1.0.0", + "description": "Test cases for boolean schema support in OpenAPI 3.1" + }, + "paths": { + "/test-tuple-false": { + "get": { + "operationId": "getTupleFalse", + "summary": "Test tuple with items: false (no additional items)", + "responses": { + "200": { + "description": "Strict tuple [integer, string]", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": false, + "prefixItems": [ + { "type": "integer" }, + { "type": "string" } + ] + } + } + } + } + } + } + }, + "/test-tuple-true": { + "get": { + "operationId": "getTupleTrue", + "summary": "Test tuple with items: true (any additional items)", + "responses": { + "200": { + "description": "Tuple with any additional items", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": true, + "prefixItems": [ + { "type": "integer" }, + { "type": "string" } + ] + } + } + } + } + } + } + }, + "/test-nested-tuple": { + "get": { + "operationId": "getNestedTuple", + "summary": "Test nested arrays with boolean schemas", + "responses": { + "200": { + "description": "Array of strict tuples", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "array", + "items": false, + "prefixItems": [ + { "type": "number", "description": "timestamp" }, + { "type": "number", "description": "value" } + ] + } + } + } + } + } + } + } + }, + "/test-mixed-schemas": { + "get": { + "operationId": "getMixedSchemas", + "summary": "Test response with both boolean and object schemas", + "responses": { + "200": { + "description": "Mixed schema types", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "strictTuple": { + "type": "array", + "items": false, + "prefixItems": [ + { "type": "string" } + ] + }, + "openTuple": { + "type": "array", + "items": true, + "prefixItems": [ + { "type": "string" } + ] + }, + "normalArray": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test-fixtures/test-boolean-schemas.sh b/test-fixtures/test-boolean-schemas.sh new file mode 100755 index 0000000..d18a6e7 --- /dev/null +++ b/test-fixtures/test-boolean-schemas.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Test script for boolean schema support + +echo "Testing Boolean Schema Support in OpenAPI 3.1" +echo "=============================================" +echo "" + +# Build the project +echo "Building project..." +npm run build:ts > /dev/null 2>&1 + +# Test 1: Boolean schema test fixture +echo "Test 1: Boolean schema test fixture" +node dist/main.js -s test-fixtures/boolean-schema-test.json -n BooleanSchemaTestClient > /tmp/boolean-test-output.ts 2>&1 +if [ $? -eq 0 ]; then + echo "✅ SUCCESS: Generated $(wc -l < /tmp/boolean-test-output.ts) lines of code" +else + echo "❌ FAILED" + cat /tmp/boolean-test-output.ts | grep -A2 "ERROR" + exit 1 +fi + +echo "" + +# Test 2: S2 OpenAPI spec (real-world case) +echo "Test 2: S2 OpenAPI spec (real-world case)" +if [ -f /tmp/s2-openapi.json ]; then + node dist/main.js -s /tmp/s2-openapi.json -n S2Client > /tmp/s2-test-output.ts 2>&1 + if [ $? -eq 0 ]; then + echo "✅ SUCCESS: Generated $(wc -l < /tmp/s2-test-output.ts) lines of code" + else + echo "❌ FAILED" + cat /tmp/s2-test-output.ts | grep -A2 "ERROR" + exit 1 + fi +else + echo "⚠️ SKIPPED: S2 spec not found at /tmp/s2-openapi.json" +fi + +echo "" +echo "All tests passed! ✅" \ No newline at end of file