diff --git a/src/analysis/GraphAnalyzer.ts b/src/analysis/GraphAnalyzer.ts index 45e808d..8f02ce5 100644 --- a/src/analysis/GraphAnalyzer.ts +++ b/src/analysis/GraphAnalyzer.ts @@ -60,8 +60,13 @@ const depsShallow = (schema: OpenApiSchema): Set => { return new Set(schema.anyOf.flatMap(subschema => [...depsShallow(subschema)])); } + if (typeof schema.type === 'undefined') { + return new Set(); // Any type + } else if (Array.isArray(schema.type)) { + const schemaAnyOf = { anyOf: schema.type.map(type => ({ ...schema, type })) } as OpenApiSchema; + return depsShallow(schemaAnyOf); + } switch (schema.type) { - case undefined: // Any type case 'null': case 'string': case 'number': @@ -106,9 +111,13 @@ const depsDeep = (schema: OpenApiSchema, resolve: Resolve, visited: Set): D return Object.assign({}, ...schema.anyOf.flatMap(subschema => depsDeep(subschema, resolve, visited))); } + if (typeof schema.type === 'undefined') { + return {}; // Any type + } else if (Array.isArray(schema.type)) { + const schemaAnyOf = { anyOf: schema.type.map(type => ({ ...schema, type })) } as OpenApiSchema; + return depsDeep(schemaAnyOf, resolve, visited); + } switch (schema.type) { - case undefined: // Any type - return {}; case 'null': case 'string': case 'number': @@ -271,9 +280,13 @@ const _isObjectSchema = (schema: OpenApiSchema, resolve: Resolve, visited: Set ({ ...schema, type })) } as OpenApiSchema; + return _isObjectSchema(schemaAnyOf, resolve, visited); + } switch (schema.type) { - case undefined: // Any type - return false; // Possibly an object, but we cannot know case 'null': case 'string': case 'number': diff --git a/src/generation/effSchemGen/schemaGen.ts b/src/generation/effSchemGen/schemaGen.ts index 4cfc2dd..ba7cf30 100644 --- a/src/generation/effSchemGen/schemaGen.ts +++ b/src/generation/effSchemGen/schemaGen.ts @@ -325,7 +325,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul } else { // Case: OpenApi.SchemaObject if (schema.type === 'array' || 'items' in schema) { // Case: OpenApi.ArraySchemaObject return generateForArraySchema(ctx, { items: {}, ...schema } as OpenApi.ArraySchemaObject); - } else if (isNonArraySchemaType(schema)) { // Case: OpenApi.NonArraySchemaObject + } else { // Case: OpenApi.NonArraySchemaObject | OpenApi.MixedSchemaObject if ('allOf' in schema && typeof schema.allOf !== 'undefined') { const schemasHead: undefined | OpenApiSchema = schema.allOf[0]; if (schemasHead && schema.allOf.length === 1) { // If only one schema, simply generate that schema @@ -485,7 +485,7 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul return dedent` ${commentsGenerated.commentBlock} ${code}, ${commentsGenerated.commentInline} - `; + `.trim(); }) .join('\n') } @@ -508,23 +508,29 @@ export const generateForSchema = (ctx: Context, schema: OpenApiSchema): GenResul if (hookResult !== null) { result = hookResult; } else { - switch (type) { - case undefined: result = generateForUnknownSchema(ctx, schema); break; - case 'null': result = generateForNullSchema(ctx, schema); break; - case 'string': result = generateForStringSchema(ctx, schema); break; - case 'number': result = generateForNumberSchema(ctx, schema); break; - case 'integer': result = generateForNumberSchema(ctx, schema); break; - case 'boolean': result = generateForBooleanSchema(ctx, schema); break; - case 'object': result = generateForObjectSchema(ctx, schema); break; - default: throw new TypeError(`Unsupported type "${type}"`); + if (typeof type === 'undefined') { + result = generateForUnknownSchema(ctx, schema as OpenApi.NonArraySchemaObject); // Any type + } else if (Array.isArray(type)) { + // `type` as an array is equivalent to `anyOf` with `type` set to the individual type string + const schemaAnyOf = { anyOf: type.map(type => ({ ...schema, type })) } as OpenApiSchema; + result = generateForSchema(ctx, schemaAnyOf); + } else { + const schemaNonMixed = schema as OpenApi.NonArraySchemaObject; + switch (type) { + case 'null': result = generateForNullSchema(ctx, schemaNonMixed); break; + case 'string': result = generateForStringSchema(ctx, schemaNonMixed); break; + case 'number': result = generateForNumberSchema(ctx, schemaNonMixed); break; + case 'integer': result = generateForNumberSchema(ctx, schemaNonMixed); break; + case 'boolean': result = generateForBooleanSchema(ctx, schemaNonMixed); break; + case 'object': result = generateForObjectSchema(ctx, schemaNonMixed); break; + default: throw new TypeError(`Unsupported type "${type}"`); + } } } return { ...result, code: `${result.code}`, }; - } else { // Case: OpenApi.MixedSchemaObject - throw new Error(`Currently unsupported: MixedSchemaObject`); } } }; diff --git a/src/openapiToEffect.ts b/src/openapiToEffect.ts index ecb5130..2198802 100644 --- a/src/openapiToEffect.ts +++ b/src/openapiToEffect.ts @@ -458,9 +458,8 @@ export const run = async (argsRaw: Array): Promise => { }); }; -const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script - // Detect if this module is being run directly from the command line +const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script if (argScript && await fs.realpath(argScript) === fileURLToPath(import.meta.url)) { try { await run(args); diff --git a/tests/fixtures/fixture0_api.json b/tests/fixtures/fixture0_api.json index 031a07f..42c5f71 100644 --- a/tests/fixtures/fixture0_api.json +++ b/tests/fixtures/fixture0_api.json @@ -10,6 +10,8 @@ "type": "object", "properties": { "name": { "type": "string" }, + "description": { "type": ["null", "string"] }, + "status": { "type": ["null", "string"], "enum": ["ACTIVE", "DEPRIORITIZED"] }, "subcategories": { "type": "object", "additionalProperties": { @@ -18,7 +20,7 @@ "default": {} } }, - "required": ["name"] + "required": ["name", "description"] }, "User": { "type": "object", @@ -34,7 +36,8 @@ }, "last_logged_in": { "title": "When the user last logged in.", - "type": "string", "format": "date-time" + "type": "string", + "format": "date-time" }, "role": { "title": "The user's role within the system.", diff --git a/tests/fixtures/fixture0_spec.ts b/tests/fixtures/fixture0_spec.ts index 5d9b4ad..665d31e 100644 --- a/tests/fixtures/fixture0_spec.ts +++ b/tests/fixtures/fixture0_spec.ts @@ -14,10 +14,14 @@ export default { schemaId: 'Category', typeDeclarationEncoded: `{ readonly name: string, + readonly description: null | string, + readonly status?: undefined | null | 'ACTIVE' | 'DEPRIORITIZED', readonly subcategories?: undefined | { readonly [key: string]: _CategoryEncoded } }`, typeDeclaration: `{ readonly name: string, + readonly description: null | string, + readonly status?: undefined | null | 'ACTIVE' | 'DEPRIORITIZED', readonly subcategories: { readonly [key: string]: _Category } }`, }, diff --git a/tests/integration/fixture0.test.ts b/tests/integration/fixture0.test.ts index 45718dd..d36d773 100644 --- a/tests/integration/fixture0.test.ts +++ b/tests/integration/fixture0.test.ts @@ -22,7 +22,15 @@ test('fixture0', { timeout: 30_000/*ms*/ }, async (t) => { const before = async () => { const cwd = path.dirname(fileURLToPath(import.meta.url)); console.log('Preparing fixture0...'); - const { stdout, stderr } = await exec(`./generate_fixture.sh fixture0`, { cwd }); + + try { + const { stdout, stderr } = await exec(`./generate_fixture.sh fixture0`, { cwd }); + } catch (error: unknown) { + if (error instanceof Error && 'stderr' in error) { + console.error(error.stderr); + } + throw error; + } }; await before(); @@ -36,14 +44,14 @@ test('fixture0', { timeout: 30_000/*ms*/ }, async (t) => { last_logged_in: '2024-05-25T19:20:39.482Z', role: 'USER', interests: [ - { name: 'Music' }, + { name: 'Music', description: null }, ], }; assert.deepStrictEqual(S.decodeUnknownSync(fixture.User)(user1), { ...user1, last_logged_in: new Date('2024-05-25T19:20:39.482Z'), // Transformed to Date interests: [ - { name: 'Music', subcategories: {} }, // Added default value + { name: 'Music', description: null, subcategories: {} }, // Added default value ], }); }); diff --git a/tests/integration/fixture1.test.ts b/tests/integration/fixture1.test.ts index 53d9d3a..1067a0e 100644 --- a/tests/integration/fixture1.test.ts +++ b/tests/integration/fixture1.test.ts @@ -22,7 +22,15 @@ test('fixture1', { timeout: 30_000/*ms*/ }, async (t) => { const before = async () => { const cwd = path.dirname(fileURLToPath(import.meta.url)); console.log('Preparing fixture1...'); - const { stdout, stderr } = await exec(`./generate_fixture.sh fixture1`, { cwd }); + + try { + const { stdout, stderr } = await exec(`./generate_fixture.sh fixture1`, { cwd }); + } catch (error: unknown) { + if (error instanceof Error && 'stderr' in error) { + console.error(error.stderr); + } + throw error; + } }; await before();