From 54feec5087f8ec03eb70f75e5027b4e65ec8e0c9 Mon Sep 17 00:00:00 2001 From: Egor Smirnov Date: Fri, 30 Oct 2026 14:28:06 +0300 Subject: [PATCH] fix: generate named union types referenced by arrays --- .gitignore | 1 + src/generators/APITypingsGenerator.test.ts | 137 +++++++++++++++++++++ src/generators/APITypingsGenerator.ts | 2 +- 3 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 src/generators/APITypingsGenerator.test.ts diff --git a/.gitignore b/.gitignore index f06235c..aee5236 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +.swe-helper/ diff --git a/src/generators/APITypingsGenerator.test.ts b/src/generators/APITypingsGenerator.test.ts new file mode 100644 index 0000000..0fa464e --- /dev/null +++ b/src/generators/APITypingsGenerator.test.ts @@ -0,0 +1,137 @@ +import path from 'path'; +import { Dictionary } from '../types'; +import { APITypingsGenerator } from './APITypingsGenerator'; +import { SchemaObject } from './SchemaObject'; + +function generateObjects(value: object, additionalObjects: Dictionary = {}) { + const objects: Dictionary = { + example_value: value, + example_container: { + type: 'object', + properties: { + values: { + type: 'array', + items: { $ref: '#/definitions/example_value' }, + }, + }, + }, + ...additionalObjects, + }; + const generator = new APITypingsGenerator({ + needEmit: false, + outDirPath: '', + methodsPattern: '*', + methodsDefinitions: { + methods: [ + { + name: 'example.get', + access_token_type: ['user'], + parameters: [ + { + name: 'items', + type: 'array', + items: { $ref: '#/definitions/example_container' }, + }, + ], + responses: { response: { $ref: '#/definitions/base_ok_response' } }, + }, + ], + }, + objects: objects as Dictionary, + responses: {}, + errors: {}, + }); + + generator.generate(); + + return generator.resultFiles; +} + +beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => undefined); + jest.spyOn(process, 'exit').mockImplementation((code) => { + throw new Error(`process.exit(${code})`); + }); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test.each([ + { + name: 'a type array with oneOf', + schema: { + type: ['string', 'integer', 'boolean', 'object', 'array'], + oneOf: [ + { type: 'string' }, + { type: 'integer' }, + { type: 'boolean' }, + { type: 'object', additionalProperties: true }, + { type: 'array', items: { type: 'object', additionalProperties: true } }, + ], + }, + expected: + 'string | number | boolean | { [key: string]: unknown } | { [key: string]: unknown }[]', + }, + { + name: 'a type array without oneOf', + schema: { type: ['string', 'integer', 'boolean'] }, + expected: 'string | number | boolean', + }, + { + name: 'oneOf without a type', + schema: { oneOf: [{ type: 'string' }, { type: 'integer' }] }, + expected: 'string | number', + }, + { + name: 'oneOf with an object type', + schema: { + type: 'object', + oneOf: [{ type: 'object' }, { type: 'array', items: { type: 'integer' } }], + }, + expected: '{ [key: string]: unknown } | number[]', + }, +])('generates a named union for $name referenced by array items', ({ schema, expected }) => { + const files = generateObjects(schema); + + expect(files[path.join('objects', 'example', 'ExampleValue.ts')]).toContain( + `export type ExampleValue = ${expected};`, + ); + expect(files[path.join('objects', 'example', 'ExampleContainer.ts')]).toContain( + "import { ExampleValue } from './ExampleValue';", + ); + expect(files[path.join('objects', 'example', 'ExampleContainer.ts')]).toContain( + 'values?: ExampleValue[];', + ); +}); + +test('generates and imports objects referenced by a named union', () => { + const files = generateObjects( + { + oneOf: [ + { type: 'string' }, + { $ref: '#/definitions/example_payload' }, + { type: 'array', items: { $ref: '#/definitions/example_payload' } }, + ], + }, + { example_payload: { type: 'object', properties: { id: { type: 'integer' } } } }, + ); + + expect(files[path.join('objects', 'example', 'ExampleValue.ts')]).toContain( + "import { ExamplePayload } from './ExamplePayload';", + ); + expect(files[path.join('objects', 'example', 'ExampleValue.ts')]).toContain( + 'export type ExampleValue = string | ExamplePayload | ExamplePayload[];', + ); + expect(files[path.join('objects', 'example', 'ExamplePayload.ts')]).toContain('id?: number;'); +}); + +test('preserves interfaces for ordinary objects referenced by array items', () => { + const files = generateObjects({ type: 'object', properties: { id: { type: 'integer' } } }); + + expect(files[path.join('objects', 'example', 'ExampleValue.ts')]).toContain( + 'export interface ExampleValue {', + ); + expect(files[path.join('objects', 'example', 'ExampleValue.ts')]).toContain('id?: number;'); +}); diff --git a/src/generators/APITypingsGenerator.ts b/src/generators/APITypingsGenerator.ts index 565a09b..98027da 100644 --- a/src/generators/APITypingsGenerator.ts +++ b/src/generators/APITypingsGenerator.ts @@ -295,7 +295,7 @@ export class APITypingsGenerator { let result: GeneratorResultInterface | false = false; - if (object.ref && object.type === 'object') { + if (object.oneOf || Array.isArray(object.type) || (object.ref && object.type === 'object')) { result = this.getPrimitiveInterfaceCode(object); } else { switch (object.type) {