@devup-api/generator
Advanced tools
+7
-3
| { | ||
| "name": "@devup-api/generator", | ||
| "version": "0.1.0", | ||
| "version": "0.1.1", | ||
| "license": "Apache-2.0", | ||
| "type": "module", | ||
@@ -12,2 +13,5 @@ "exports": { | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
@@ -20,4 +24,4 @@ "build": "tsc && bun build --target node --outfile=dist/index.js src/index.ts --production --packages=external && bun build --target node --outfile=dist/index.cjs --format=cjs src/index.ts --production --packages=external" | ||
| "dependencies": { | ||
| "@devup-api/core": "0.1.0", | ||
| "@devup-api/utils": "0.1.0" | ||
| "@devup-api/core": "0.1.1", | ||
| "@devup-api/utils": "0.1.1" | ||
| }, | ||
@@ -24,0 +28,0 @@ "devDependencies": { |
| import { expect, test } from 'bun:test' | ||
| import { convertCase } from '../convert-case' | ||
| test.each([ | ||
| ['hello_world', 'snake', 'hello_world'], | ||
| ['my_variable_name', 'snake', 'my_variable_name'], | ||
| ['snake_case_string', 'snake', 'snake_case_string'], | ||
| ])('converts to snake_case: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'snake')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['HelloWorld', 'snake', 'hello_world'], | ||
| ['MyVariableName', 'snake', 'my_variable_name'], | ||
| ['PascalCaseString', 'snake', 'pascal_case_string'], | ||
| ])('converts PascalCase to snake_case: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'snake')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['helloWorld', 'snake', 'hello_world'], | ||
| ['myVariableName', 'snake', 'my_variable_name'], | ||
| ['camelCaseString', 'snake', 'camel_case_string'], | ||
| ])('converts camelCase to snake_case: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'snake')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', 'camel', 'helloWorld'], | ||
| ['my_variable_name', 'camel', 'myVariableName'], | ||
| ['snake_case_string', 'camel', 'snakeCaseString'], | ||
| ])('converts to camelCase: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'camel')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['HelloWorld', 'camel', 'helloWorld'], | ||
| ['MyVariableName', 'camel', 'myVariableName'], | ||
| ['PascalCaseString', 'camel', 'pascalCaseString'], | ||
| ])('converts PascalCase to camelCase: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'camel')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['helloWorld', 'camel', 'helloWorld'], | ||
| ['myVariableName', 'camel', 'myVariableName'], | ||
| ['camelCaseString', 'camel', 'camelCaseString'], | ||
| ])('returns camelCase strings unchanged: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'camel')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', 'pascal', 'HelloWorld'], | ||
| ['my_variable_name', 'pascal', 'MyVariableName'], | ||
| ['snake_case_string', 'pascal', 'SnakeCaseString'], | ||
| ])('converts to PascalCase: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'pascal')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['helloWorld', 'pascal', 'HelloWorld'], | ||
| ['myVariableName', 'pascal', 'MyVariableName'], | ||
| ['camelCaseString', 'pascal', 'CamelCaseString'], | ||
| ])('converts camelCase to PascalCase: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'pascal')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['HelloWorld', 'pascal', 'HelloWorld'], | ||
| ['MyVariableName', 'pascal', 'MyVariableName'], | ||
| ['PascalCaseString', 'pascal', 'PascalCaseString'], | ||
| ])('returns PascalCase strings unchanged: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'pascal')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', 'maintain', 'hello_world'], | ||
| ['myVariableName', 'maintain', 'myVariableName'], | ||
| ['HelloWorld', 'maintain', 'HelloWorld'], | ||
| ['any_string-here', 'maintain', 'any_string-here'], | ||
| ])('maintains original case: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'maintain')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', undefined, 'helloWorld'], | ||
| ['my_variable_name', undefined, 'myVariableName'], | ||
| ['snake_case_string', undefined, 'snakeCaseString'], | ||
| ])('defaults to camelCase when caseType is undefined: %s -> %s', (input, caseType, expected) => { | ||
| // biome-ignore lint/suspicious/noExplicitAny: Testing default behavior with undefined caseType | ||
| expect(convertCase(input, caseType as any)).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', 'helloWorld'], | ||
| ['my_variable_name', 'myVariableName'], | ||
| ['snake_case_string', 'snakeCaseString'], | ||
| ])('defaults to camelCase when caseType is not provided: %s -> %s', (input, expected) => { | ||
| expect(convertCase(input)).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['', 'camel', ''], | ||
| ['a', 'camel', 'a'], | ||
| ['A', 'camel', 'a'], | ||
| ])('handles empty string and single characters: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'camel')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world123', 'camel', 'helloWorld123'], | ||
| ['my_variable2_name', 'camel', 'myVariable2Name'], | ||
| ['test123_case', 'camel', 'test123Case'], | ||
| ])('handles strings with numbers: %s -> %s', (input, caseType, expected) => { | ||
| expect(convertCase(input, caseType as 'camel')).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['hello_world', 'invalid', 'hello_world'], | ||
| ['myVariableName', 'unknown', 'myVariableName'], | ||
| ['HelloWorld', 'wrong', 'HelloWorld'], | ||
| ])('default case returns original string for invalid caseType: %s -> %s', (input, caseType, expected) => { | ||
| // biome-ignore lint/suspicious/noExplicitAny: Testing default case with invalid caseType values | ||
| expect(convertCase(input, caseType as any)).toBe(expected) | ||
| }) |
| import { expect, test } from 'bun:test' | ||
| import type { UrlMapValue } from '@devup-api/core' | ||
| import type { OpenAPIV3_1 } from 'openapi-types' | ||
| import { createUrlMap } from '../create-url-map' | ||
| test.each([ | ||
| [ | ||
| 'camel', | ||
| undefined, | ||
| 'get_users', | ||
| { | ||
| getUsers: { method: 'GET', url: '/users' }, | ||
| '/users': { method: 'GET', url: '/users' }, | ||
| }, | ||
| ], | ||
| [ | ||
| 'snake', | ||
| { convertCase: 'snake' as const }, | ||
| 'getUsers', | ||
| { | ||
| get_users: { method: 'GET', url: '/users' }, | ||
| '/users': { method: 'GET', url: '/users' }, | ||
| }, | ||
| ], | ||
| [ | ||
| 'pascal', | ||
| { convertCase: 'pascal' as const }, | ||
| 'get_users', | ||
| { | ||
| GetUsers: { method: 'GET', url: '/users' }, | ||
| '/users': { method: 'GET', url: '/users' }, | ||
| }, | ||
| ], | ||
| [ | ||
| 'maintain', | ||
| { convertCase: 'maintain' as const }, | ||
| 'get_users', | ||
| { | ||
| get_users: { method: 'GET', url: '/users' }, | ||
| '/users': { method: 'GET', url: '/users' }, | ||
| }, | ||
| ], | ||
| ])('creates url map with %s case conversion', (_, options, operationId, expected) => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| get: { | ||
| operationId, | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema, options) | ||
| expect(result).toEqual(expected as Record<string, UrlMapValue>) | ||
| }) | ||
| test('converts path parameters based on convertCase', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users/{user_id}/posts/{post_id}': { | ||
| get: { | ||
| operationId: 'get_user_post', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema, { convertCase: 'camel' }) | ||
| expect(result).toEqual({ | ||
| getUserPost: { | ||
| method: 'GET', | ||
| url: '/users/{userId}/posts/{postId}', | ||
| }, | ||
| '/users/{userId}/posts/{postId}': { | ||
| method: 'GET', | ||
| url: '/users/{userId}/posts/{postId}', | ||
| }, | ||
| }) | ||
| }) | ||
| test.each([ | ||
| ['get', 'get_users', 'getUsers', 'GET'], | ||
| ['post', 'create_user', 'createUser', 'POST'], | ||
| ['put', 'update_user', 'updateUser', 'PUT'], | ||
| ['delete', 'delete_user', 'deleteUser', 'DELETE'], | ||
| ['patch', 'patch_user', 'patchUser', 'PATCH'], | ||
| ])('handles %s HTTP method', (method, operationId, expectedKey, expectedMethod) => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| [method]: { | ||
| operationId, | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toHaveProperty(expectedKey) | ||
| expect(result[expectedKey]?.method).toBe( | ||
| expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', | ||
| ) | ||
| expect(result).toHaveProperty('/users') | ||
| expect(result['/users']?.method).toBe( | ||
| expectedMethod as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH', | ||
| ) | ||
| }) | ||
| test('handles operation without operationId', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| get: { | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toEqual({ | ||
| '/users': { | ||
| method: 'GET', | ||
| url: '/users', | ||
| }, | ||
| }) | ||
| expect(result).not.toHaveProperty('getUsers') | ||
| }) | ||
| test('handles multiple paths', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| get: { | ||
| operationId: 'get_users', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| '/posts': { | ||
| get: { | ||
| operationId: 'get_posts', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toHaveProperty('getUsers') | ||
| expect(result).toHaveProperty('getPosts') | ||
| expect(result).toHaveProperty('/users') | ||
| expect(result).toHaveProperty('/posts') | ||
| }) | ||
| test('handles empty paths', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: {}, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toEqual({}) | ||
| }) | ||
| test('handles undefined paths', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| components: {}, | ||
| paths: {}, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toEqual({}) | ||
| }) | ||
| test('handles undefined pathItem', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': undefined, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toEqual({}) | ||
| }) | ||
| test('skips operations that do not exist', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| get: { | ||
| operationId: 'get_users', | ||
| responses: {}, | ||
| }, | ||
| // post, put, delete, patch are not defined | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema) | ||
| expect(result).toEqual({ | ||
| getUsers: { | ||
| method: 'GET', | ||
| url: '/users', | ||
| }, | ||
| '/users': { | ||
| method: 'GET', | ||
| url: '/users', | ||
| }, | ||
| }) | ||
| }) | ||
| test('handles complex path with multiple parameters', () => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': { | ||
| get: { | ||
| operationId: 'get_user_post_comment', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema, { convertCase: 'snake' }) | ||
| expect(result).toEqual({ | ||
| get_user_post_comment: { | ||
| method: 'GET', | ||
| url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', | ||
| }, | ||
| '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}': { | ||
| method: 'GET', | ||
| url: '/api/v1/users/{user_id}/posts/{post_id}/comments/{comment_id}', | ||
| }, | ||
| }) | ||
| }) | ||
| test.each([ | ||
| ['camel', '/users/{userId}', '/users/{userId}'], | ||
| ['snake', '/users/{user_id}', '/users/{user_id}'], | ||
| ['pascal', '/users/{UserId}', '/users/{UserId}'], | ||
| ])('converts path parameters with %s case: %s', (caseType, expectedPath, expectedUrl) => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users/{user_id}': { | ||
| get: { | ||
| operationId: 'get_user', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema, { | ||
| convertCase: caseType as 'camel' | 'snake' | 'pascal', | ||
| }) | ||
| expect(result[expectedPath]?.url).toBe(expectedUrl) | ||
| }) | ||
| test.each([ | ||
| ['camel', 'getUserList'], | ||
| ['snake', 'get_user_list'], | ||
| ['pascal', 'GetUserList'], | ||
| ])('converts operationId with %s case: %s', (caseType, expectedKey) => { | ||
| const schema: OpenAPIV3_1.Document = { | ||
| openapi: '3.1.0', | ||
| info: { title: 'Test API', version: '1.0.0' }, | ||
| paths: { | ||
| '/users': { | ||
| get: { | ||
| operationId: 'get_user_list', | ||
| responses: {}, | ||
| }, | ||
| }, | ||
| }, | ||
| } | ||
| const result = createUrlMap(schema, { | ||
| convertCase: caseType as 'camel' | 'snake' | 'pascal', | ||
| }) | ||
| expect(result).toHaveProperty(expectedKey) | ||
| }) |
| import { expect, test } from 'bun:test' | ||
| import * as indexModule from '../index' | ||
| test('index.ts exports', () => { | ||
| expect({ ...indexModule }).toEqual({ | ||
| createUrlMap: expect.any(Function), | ||
| generateInterface: expect.any(Function), | ||
| }) | ||
| }) |
| import { expect, test } from 'bun:test' | ||
| import { wrapInterfaceKeyGuard } from '../wrap-interface-key-guard' | ||
| test.each([ | ||
| ['getUsers', 'getUsers'], | ||
| ['createUser', 'createUser'], | ||
| ['updateUser', 'updateUser'], | ||
| ['deleteUser', 'deleteUser'], | ||
| ['testKey', 'testKey'], | ||
| ['camelCase', 'camelCase'], | ||
| ['snake_case', 'snake_case'], | ||
| ['PascalCase', 'PascalCase'], | ||
| ] as const)('wrapInterfaceKeyGuard returns key as-is when no slash: %s -> %s', (key, expected) => { | ||
| expect(wrapInterfaceKeyGuard(key)).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['/users', '[`/users`]'], | ||
| ['/users/{id}', '[`/users/{id}`]'], | ||
| ['/api/v1/users', '[`/api/v1/users`]'], | ||
| ['/users/{userId}/posts/{postId}', '[`/users/{userId}/posts/{postId}`]'], | ||
| ['/api/v1/users/{id}/profile', '[`/api/v1/users/{id}/profile`]'], | ||
| ] as const)('wrapInterfaceKeyGuard wraps key with backticks when slash present: %s -> %s', (key, expected) => { | ||
| expect(wrapInterfaceKeyGuard(key)).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['', ''], | ||
| ['/', '[`/`]'], | ||
| ['//', '[`//`]'], | ||
| ['///', '[`///`]'], | ||
| ] as const)('wrapInterfaceKeyGuard handles edge cases: %s -> %s', (key, expected) => { | ||
| expect(wrapInterfaceKeyGuard(key)).toBe(expected) | ||
| }) | ||
| test.each([ | ||
| ['users/123', '[`users/123`]'], | ||
| ['test/path/here', '[`test/path/here`]'], | ||
| ['a/b/c/d', '[`a/b/c/d`]'], | ||
| ] as const)('wrapInterfaceKeyGuard wraps key with multiple slashes: %s -> %s', (key, expected) => { | ||
| expect(wrapInterfaceKeyGuard(key)).toBe(expected) | ||
| }) |
| import { toCamel, toPascal, toSnake } from '@devup-api/utils' | ||
| /** | ||
| * Convert string based on convertCase option | ||
| */ | ||
| export function convertCase( | ||
| str: string, | ||
| caseType: 'snake' | 'camel' | 'pascal' | 'maintain' = 'camel', | ||
| ): string { | ||
| switch (caseType) { | ||
| case 'snake': | ||
| return toSnake(str) | ||
| case 'camel': | ||
| return toCamel(str) | ||
| case 'pascal': | ||
| return toPascal(str) | ||
| case 'maintain': | ||
| return str | ||
| default: | ||
| return str | ||
| } | ||
| } |
| import type { DevupApiTypeGeneratorOptions, UrlMapValue } from '@devup-api/core' | ||
| import type { OpenAPIV3_1 } from 'openapi-types' | ||
| import { convertCase } from './convert-case' | ||
| export function createUrlMap( | ||
| schema: OpenAPIV3_1.Document, | ||
| options?: DevupApiTypeGeneratorOptions, | ||
| ) { | ||
| const convertCaseType = options?.convertCase ?? 'camel' | ||
| const urlMap: Record<string, UrlMapValue> = {} | ||
| for (const [path, pathItem] of Object.entries(schema.paths ?? {})) { | ||
| if (!pathItem) continue | ||
| for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) { | ||
| const operation = pathItem[method] | ||
| if (!operation) continue | ||
| const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => { | ||
| // Convert param name based on case type | ||
| return `{${convertCase(param, convertCaseType)}}` | ||
| }) | ||
| if (operation.operationId) { | ||
| urlMap[convertCase(operation.operationId, convertCaseType)] = { | ||
| method: method.toUpperCase() as | ||
| | 'GET' | ||
| | 'POST' | ||
| | 'PUT' | ||
| | 'DELETE' | ||
| | 'PATCH', | ||
| url: normalizedPath, | ||
| } | ||
| } | ||
| urlMap[normalizedPath] = { | ||
| method: method.toUpperCase() as | ||
| | 'GET' | ||
| | 'POST' | ||
| | 'PUT' | ||
| | 'DELETE' | ||
| | 'PATCH', | ||
| url: normalizedPath, | ||
| } | ||
| } | ||
| } | ||
| return urlMap | ||
| } |
| import type { DevupApiTypeGeneratorOptions } from '@devup-api/core' | ||
| import { toPascal } from '@devup-api/utils' | ||
| import type { OpenAPIV3_1 } from 'openapi-types' | ||
| import { convertCase } from './convert-case' | ||
| import { | ||
| extractParameters, | ||
| extractRequestBody, | ||
| formatTypeValue, | ||
| getTypeFromSchema, | ||
| } from './generate-schema' | ||
| import { wrapInterfaceKeyGuard } from './wrap-interface-key-guard' | ||
| export interface ParameterDefinition | ||
| extends Omit<OpenAPIV3_1.ParameterObject, 'schema'> { | ||
| type: unknown | ||
| default?: unknown | ||
| } | ||
| export interface EndpointDefinition { | ||
| params?: Record<string, ParameterDefinition> | ||
| body?: unknown | ||
| query?: Record<string, ParameterDefinition> | ||
| response?: unknown | ||
| error?: unknown | ||
| } | ||
| // Helper function to extract schema names from $ref | ||
| function extractSchemaNameFromRef(ref: string): string | null { | ||
| if (ref.startsWith('#/components/schemas/')) { | ||
| return ref.replace('#/components/schemas/', '') | ||
| } | ||
| return null | ||
| } | ||
| export function generateInterface( | ||
| schema: OpenAPIV3_1.Document, | ||
| options?: DevupApiTypeGeneratorOptions, | ||
| ): string { | ||
| const endpoints: Record< | ||
| 'get' | 'post' | 'put' | 'delete' | 'patch', | ||
| Record<string, EndpointDefinition> | ||
| > = { | ||
| get: {}, | ||
| post: {}, | ||
| put: {}, | ||
| delete: {}, | ||
| patch: {}, | ||
| } as const | ||
| const convertCaseType = options?.convertCase ?? 'camel' | ||
| // Helper function to collect schema names from a schema object | ||
| const collectSchemaNames = ( | ||
| schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, | ||
| targetSet: Set<string>, | ||
| ): void => { | ||
| if ('$ref' in schemaObj) { | ||
| const schemaName = extractSchemaNameFromRef(schemaObj.$ref) | ||
| if (schemaName) { | ||
| targetSet.add(schemaName) | ||
| } | ||
| return | ||
| } | ||
| const schema = schemaObj as OpenAPIV3_1.SchemaObject | ||
| // Check allOf, anyOf, oneOf | ||
| if (schema.allOf) { | ||
| schema.allOf.forEach((s) => { | ||
| collectSchemaNames(s, targetSet) | ||
| }) | ||
| } | ||
| if (schema.anyOf) { | ||
| schema.anyOf.forEach((s) => { | ||
| collectSchemaNames(s, targetSet) | ||
| }) | ||
| } | ||
| if (schema.oneOf) { | ||
| schema.oneOf.forEach((s) => { | ||
| collectSchemaNames(s, targetSet) | ||
| }) | ||
| } | ||
| // Check properties | ||
| if (schema.properties) { | ||
| Object.values(schema.properties).forEach((prop) => { | ||
| collectSchemaNames(prop, targetSet) | ||
| }) | ||
| } | ||
| // Check items (for arrays) | ||
| if (schema.type === 'array' && 'items' in schema && schema.items) { | ||
| collectSchemaNames(schema.items, targetSet) | ||
| } | ||
| } | ||
| // Track which schemas are used in request body and responses | ||
| const requestSchemaNames = new Set<string>() | ||
| const responseSchemaNames = new Set<string>() | ||
| const errorSchemaNames = new Set<string>() | ||
| // Helper function to check if a status code is an error response | ||
| const isErrorStatusCode = (statusCode: string): boolean => { | ||
| if (statusCode === 'default') return true | ||
| const code = parseInt(statusCode, 10) | ||
| return code >= 400 && code < 600 | ||
| } | ||
| // First, collect schema names used in request body and responses | ||
| if (schema.paths) { | ||
| for (const pathItem of Object.values(schema.paths)) { | ||
| if (!pathItem) continue | ||
| const methods = ['get', 'post', 'put', 'delete', 'patch'] as const | ||
| for (const method of methods) { | ||
| const operation = pathItem[method] | ||
| if (!operation) continue | ||
| // Collect request body schemas | ||
| if (operation.requestBody) { | ||
| if ('$ref' in operation.requestBody) { | ||
| // Extract schema name from $ref if it's a schema reference | ||
| const schemaName = extractSchemaNameFromRef( | ||
| operation.requestBody.$ref, | ||
| ) | ||
| if (schemaName) { | ||
| requestSchemaNames.add(schemaName) | ||
| } | ||
| } else { | ||
| const content = operation.requestBody.content | ||
| const jsonContent = content?.['application/json'] | ||
| if (jsonContent && 'schema' in jsonContent && jsonContent.schema) { | ||
| collectSchemaNames(jsonContent.schema, requestSchemaNames) | ||
| } | ||
| } | ||
| } | ||
| // Collect response and error schemas | ||
| if (operation.responses) { | ||
| for (const [statusCode, response] of Object.entries( | ||
| operation.responses, | ||
| )) { | ||
| const isError = isErrorStatusCode(statusCode) | ||
| if ('$ref' in response) { | ||
| // Extract schema name from $ref if it's a schema reference | ||
| const schemaName = extractSchemaNameFromRef(response.$ref) | ||
| if (schemaName) { | ||
| if (isError) { | ||
| errorSchemaNames.add(schemaName) | ||
| } else { | ||
| responseSchemaNames.add(schemaName) | ||
| } | ||
| } | ||
| } else if ('content' in response) { | ||
| const content = response.content | ||
| const jsonContent = content?.['application/json'] | ||
| if ( | ||
| jsonContent && | ||
| 'schema' in jsonContent && | ||
| jsonContent.schema | ||
| ) { | ||
| if (isError) { | ||
| collectSchemaNames(jsonContent.schema, errorSchemaNames) | ||
| } else { | ||
| collectSchemaNames(jsonContent.schema, responseSchemaNames) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Iterate through OpenAPI paths and extract each endpoint | ||
| if (schema.paths) { | ||
| for (const [path, pathItem] of Object.entries(schema.paths)) { | ||
| if (!pathItem) continue | ||
| // Process each HTTP method | ||
| const methods = ['get', 'post', 'put', 'delete', 'patch'] as const | ||
| for (const method of methods) { | ||
| const operation = pathItem[method] | ||
| if (!operation) continue | ||
| const endpoint: EndpointDefinition = {} | ||
| // Extract parameters (path, query, header) | ||
| const { pathParams, queryParams } = extractParameters( | ||
| pathItem, | ||
| operation, | ||
| schema, | ||
| ) | ||
| // Apply case conversion to parameter names | ||
| const convertedPathParams: Record<string, ParameterDefinition> = {} | ||
| for (const [key, value] of Object.entries(pathParams)) { | ||
| const convertedKey = convertCase(key, convertCaseType) | ||
| convertedPathParams[convertedKey] = value | ||
| } | ||
| const convertedQueryParams: Record<string, ParameterDefinition> = {} | ||
| for (const [key, value] of Object.entries(queryParams)) { | ||
| const convertedKey = convertCase(key, convertCaseType) | ||
| convertedQueryParams[convertedKey] = value | ||
| } | ||
| if (Object.keys(convertedPathParams).length > 0) { | ||
| endpoint.params = convertedPathParams | ||
| } | ||
| if (Object.keys(convertedQueryParams).length > 0) { | ||
| endpoint.query = convertedQueryParams | ||
| } | ||
| // Extract request body | ||
| // Check if request body uses a component schema | ||
| let requestBodyType: unknown | ||
| if (operation.requestBody) { | ||
| if ('$ref' in operation.requestBody) { | ||
| // RequestBodyObject reference - skip for now | ||
| const requestBody = extractRequestBody( | ||
| operation.requestBody, | ||
| schema, | ||
| ) | ||
| if (requestBody !== undefined) { | ||
| requestBodyType = requestBody | ||
| } | ||
| } else { | ||
| const content = operation.requestBody.content | ||
| const jsonContent = content?.['application/json'] | ||
| if (jsonContent && 'schema' in jsonContent && jsonContent.schema) { | ||
| // Check if schema is a direct reference to components.schemas | ||
| if ('$ref' in jsonContent.schema) { | ||
| const schemaName = extractSchemaNameFromRef( | ||
| jsonContent.schema.$ref, | ||
| ) | ||
| // Check if schema exists in components.schemas and is used in request body | ||
| if ( | ||
| schemaName && | ||
| schema.components?.schemas?.[schemaName] && | ||
| requestSchemaNames.has(schemaName) | ||
| ) { | ||
| // Use component reference | ||
| requestBodyType = `DevupRequestComponentStruct['${schemaName}']` | ||
| } else { | ||
| const requestBody = extractRequestBody( | ||
| operation.requestBody, | ||
| schema, | ||
| ) | ||
| if (requestBody !== undefined) { | ||
| requestBodyType = requestBody | ||
| } | ||
| } | ||
| } else { | ||
| const requestBody = extractRequestBody( | ||
| operation.requestBody, | ||
| schema, | ||
| ) | ||
| if (requestBody !== undefined) { | ||
| requestBodyType = requestBody | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (requestBodyType !== undefined) { | ||
| endpoint.body = requestBodyType | ||
| } | ||
| // Extract response | ||
| // Check if response uses a component schema | ||
| let responseType: unknown | ||
| if (operation.responses) { | ||
| // Prefer 200 response, fallback to first available response | ||
| const successResponse = | ||
| operation.responses['200'] || | ||
| operation.responses['201'] || | ||
| Object.values(operation.responses)[0] | ||
| if (successResponse) { | ||
| if ('$ref' in successResponse) { | ||
| // ResponseObject reference - skip for now | ||
| // Could resolve if needed | ||
| } else if ('content' in successResponse) { | ||
| const content = successResponse.content | ||
| const jsonContent = content?.['application/json'] | ||
| if ( | ||
| jsonContent && | ||
| 'schema' in jsonContent && | ||
| jsonContent.schema | ||
| ) { | ||
| // Check if schema is a direct reference to components.schemas | ||
| if ('$ref' in jsonContent.schema) { | ||
| const schemaName = extractSchemaNameFromRef( | ||
| jsonContent.schema.$ref, | ||
| ) | ||
| // Check if schema exists in components.schemas and is used in response | ||
| if ( | ||
| schemaName && | ||
| schema.components?.schemas?.[schemaName] && | ||
| responseSchemaNames.has(schemaName) | ||
| ) { | ||
| // Use component reference | ||
| responseType = `DevupResponseComponentStruct['${schemaName}']` | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| responseType = schemaType | ||
| } | ||
| } else { | ||
| // Check if it's an array with items referencing a component schema | ||
| const schemaObj = | ||
| jsonContent.schema as OpenAPIV3_1.SchemaObject | ||
| if ( | ||
| schemaObj.type === 'array' && | ||
| schemaObj.items && | ||
| '$ref' in schemaObj.items | ||
| ) { | ||
| const schemaName = extractSchemaNameFromRef( | ||
| schemaObj.items.$ref, | ||
| ) | ||
| // Check if schema exists in components.schemas and is used in response | ||
| if ( | ||
| schemaName && | ||
| schema.components?.schemas?.[schemaName] && | ||
| responseSchemaNames.has(schemaName) | ||
| ) { | ||
| // Use component reference for array items | ||
| responseType = `Array<DevupResponseComponentStruct['${schemaName}']>` | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| responseType = schemaType | ||
| } | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| responseType = schemaType | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (responseType !== undefined) { | ||
| endpoint.response = responseType | ||
| } | ||
| // Extract error | ||
| // Check if error uses a component schema | ||
| let errorType: unknown | ||
| if (operation.responses) { | ||
| // Find error responses (4xx, 5xx, or default) | ||
| const errorResponse = | ||
| operation.responses['400'] || | ||
| operation.responses['401'] || | ||
| operation.responses['403'] || | ||
| operation.responses['404'] || | ||
| operation.responses['422'] || | ||
| operation.responses['500'] || | ||
| operation.responses.default || | ||
| Object.entries(operation.responses).find(([statusCode]) => | ||
| isErrorStatusCode(statusCode), | ||
| )?.[1] | ||
| if (errorResponse) { | ||
| if ('$ref' in errorResponse) { | ||
| // ResponseObject reference - skip for now | ||
| // Could resolve if needed | ||
| } else if ('content' in errorResponse) { | ||
| const content = errorResponse.content | ||
| const jsonContent = content?.['application/json'] | ||
| if ( | ||
| jsonContent && | ||
| 'schema' in jsonContent && | ||
| jsonContent.schema | ||
| ) { | ||
| // Check if schema is a direct reference to components.schemas | ||
| if ('$ref' in jsonContent.schema) { | ||
| const schemaName = extractSchemaNameFromRef( | ||
| jsonContent.schema.$ref, | ||
| ) | ||
| // Check if schema exists in components.schemas and is used in error | ||
| if ( | ||
| schemaName && | ||
| schema.components?.schemas?.[schemaName] && | ||
| errorSchemaNames.has(schemaName) | ||
| ) { | ||
| // Use component reference | ||
| errorType = `DevupErrorComponentStruct['${schemaName}']` | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| errorType = schemaType | ||
| } | ||
| } else { | ||
| // Check if it's an array with items referencing a component schema | ||
| const schemaObj = | ||
| jsonContent.schema as OpenAPIV3_1.SchemaObject | ||
| if ( | ||
| schemaObj.type === 'array' && | ||
| schemaObj.items && | ||
| '$ref' in schemaObj.items | ||
| ) { | ||
| const schemaName = extractSchemaNameFromRef( | ||
| schemaObj.items.$ref, | ||
| ) | ||
| // Check if schema exists in components.schemas and is used in error | ||
| if ( | ||
| schemaName && | ||
| schema.components?.schemas?.[schemaName] && | ||
| errorSchemaNames.has(schemaName) | ||
| ) { | ||
| // Use component reference for array items | ||
| errorType = `Array<DevupErrorComponentStruct['${schemaName}']>` | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| errorType = schemaType | ||
| } | ||
| } else { | ||
| // Extract schema type with response options | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| jsonContent.schema, | ||
| schema, | ||
| { defaultNonNullable: responseDefaultNonNullable }, | ||
| ) | ||
| errorType = schemaType | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (errorType !== undefined) { | ||
| endpoint.error = errorType | ||
| } | ||
| // Generate path key (normalize path by replacing {param} with converted param and removing slashes) | ||
| const normalizedPath = path.replace(/\{([^}]+)\}/g, (_, param) => { | ||
| // Convert param name based on case type | ||
| return `{${convertCase(param, convertCaseType)}}` | ||
| }) | ||
| endpoints[method][normalizedPath] = endpoint | ||
| if (operation.operationId) { | ||
| // If operationId exists, create both operationId and path keys | ||
| const operationIdKey = convertCase( | ||
| operation.operationId, | ||
| convertCaseType, | ||
| ) | ||
| endpoints[method][operationIdKey] = endpoint | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Extract components schemas | ||
| const requestComponents: Record<string, unknown> = {} | ||
| const responseComponents: Record<string, unknown> = {} | ||
| const errorComponents: Record<string, unknown> = {} | ||
| if (schema.components?.schemas) { | ||
| for (const [schemaName, schemaObj] of Object.entries( | ||
| schema.components.schemas, | ||
| )) { | ||
| if (schemaObj) { | ||
| const requestDefaultNonNullable = | ||
| options?.requestDefaultNonNullable ?? false | ||
| const responseDefaultNonNullable = | ||
| options?.responseDefaultNonNullable ?? true | ||
| // Determine which defaultNonNullable to use based on where schema is used | ||
| let defaultNonNullable = responseDefaultNonNullable | ||
| if (requestSchemaNames.has(schemaName)) { | ||
| defaultNonNullable = requestDefaultNonNullable | ||
| } | ||
| const { type: schemaType } = getTypeFromSchema( | ||
| schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, | ||
| schema, | ||
| { defaultNonNullable }, | ||
| ) | ||
| // Keep original schema name as-is | ||
| if (requestSchemaNames.has(schemaName)) { | ||
| requestComponents[schemaName] = schemaType | ||
| } | ||
| if (responseSchemaNames.has(schemaName)) { | ||
| responseComponents[schemaName] = schemaType | ||
| } | ||
| if (errorSchemaNames.has(schemaName)) { | ||
| errorComponents[schemaName] = schemaType | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Generate TypeScript interface string | ||
| const interfaceContent = Object.entries(endpoints) | ||
| .flatMap(([method, value]) => { | ||
| const entries = Object.entries(value) | ||
| if (entries.length > 0) { | ||
| const interfaceEntries = entries | ||
| .map(([key, endpointValue]) => { | ||
| const formattedValue = formatTypeValue(endpointValue, 2) | ||
| // Top-level keys in ApiStruct should never be optional | ||
| // Only params, query, body etc. can be optional if all their properties are optional | ||
| return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}` | ||
| }) | ||
| .join(';\n') | ||
| return [ | ||
| ` interface Devup${toPascal(method)}ApiStruct {\n${interfaceEntries};\n }`, | ||
| ] | ||
| } | ||
| return [] | ||
| }) | ||
| .join('\n') | ||
| // Generate RequestComponentStruct interface | ||
| const requestComponentEntries = Object.entries(requestComponents) | ||
| .map(([key, value]) => { | ||
| const formattedValue = formatTypeValue(value, 2) | ||
| return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}` | ||
| }) | ||
| .join(';\n') | ||
| const requestComponentInterface = | ||
| requestComponentEntries.length > 0 | ||
| ? ` interface DevupRequestComponentStruct {\n${requestComponentEntries};\n }` | ||
| : ' interface DevupRequestComponentStruct {}' | ||
| // Generate ResponseComponentStruct interface | ||
| const responseComponentEntries = Object.entries(responseComponents) | ||
| .map(([key, value]) => { | ||
| const formattedValue = formatTypeValue(value, 2) | ||
| return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}` | ||
| }) | ||
| .join(';\n') | ||
| const responseComponentInterface = | ||
| responseComponentEntries.length > 0 | ||
| ? ` interface DevupResponseComponentStruct {\n${responseComponentEntries};\n }` | ||
| : ' interface DevupResponseComponentStruct {}' | ||
| // Generate ErrorComponentStruct interface | ||
| const errorComponentEntries = Object.entries(errorComponents) | ||
| .map(([key, value]) => { | ||
| const formattedValue = formatTypeValue(value, 2) | ||
| return ` ${wrapInterfaceKeyGuard(key)}: ${formattedValue}` | ||
| }) | ||
| .join(';\n') | ||
| const errorComponentInterface = | ||
| errorComponentEntries.length > 0 | ||
| ? ` interface DevupErrorComponentStruct {\n${errorComponentEntries};\n }` | ||
| : ' interface DevupErrorComponentStruct {}' | ||
| const allInterfaces = interfaceContent | ||
| ? `${interfaceContent}\n\n${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}` | ||
| : `${requestComponentInterface}\n\n${responseComponentInterface}\n\n${errorComponentInterface}` | ||
| return `import "@devup-api/fetch";\n\ndeclare module "@devup-api/fetch" {\n${allInterfaces}\n}` | ||
| } |
| import type { OpenAPIV3_1 } from 'openapi-types' | ||
| import type { ParameterDefinition } from './generate-interface' | ||
| /** | ||
| * Resolve $ref reference in OpenAPI parameter | ||
| */ | ||
| export function resolveParameterRef( | ||
| ref: string, | ||
| document: OpenAPIV3_1.Document, | ||
| ): OpenAPIV3_1.ParameterObject | null { | ||
| if (!ref.startsWith('#/')) { | ||
| return null | ||
| } | ||
| const parts = ref.slice(2).split('/') | ||
| let current: unknown = document | ||
| for (const part of parts) { | ||
| if (current && typeof current === 'object' && part in current) { | ||
| current = (current as Record<string, unknown>)[part] | ||
| } else { | ||
| return null | ||
| } | ||
| } | ||
| if (current && typeof current === 'object' && !('$ref' in current)) { | ||
| return current as OpenAPIV3_1.ParameterObject | ||
| } | ||
| return null | ||
| } | ||
| /** | ||
| * Resolve $ref reference in OpenAPI schema | ||
| */ | ||
| export function resolveSchemaRef( | ||
| ref: string, | ||
| document: OpenAPIV3_1.Document, | ||
| ): OpenAPIV3_1.SchemaObject | null { | ||
| if (!ref.startsWith('#/')) { | ||
| return null | ||
| } | ||
| const parts = ref.slice(2).split('/') | ||
| let current: unknown = document | ||
| for (const part of parts) { | ||
| if (current && typeof current === 'object' && part in current) { | ||
| current = (current as Record<string, unknown>)[part] | ||
| } else { | ||
| return null | ||
| } | ||
| } | ||
| if (current && typeof current === 'object' && !('$ref' in current)) { | ||
| return current as OpenAPIV3_1.SchemaObject | ||
| } | ||
| return null | ||
| } | ||
| /** | ||
| * Convert OpenAPI schema to TypeScript type representation | ||
| */ | ||
| export function getTypeFromSchema( | ||
| schema: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, | ||
| document: OpenAPIV3_1.Document, | ||
| options?: { | ||
| defaultNonNullable?: boolean | ||
| }, | ||
| ): { type: unknown; default?: unknown } { | ||
| const defaultNonNullable = options?.defaultNonNullable ?? false | ||
| // Handle $ref | ||
| if ('$ref' in schema) { | ||
| const resolved = resolveSchemaRef(schema.$ref, document) | ||
| if (resolved) { | ||
| return getTypeFromSchema(resolved, document, options) | ||
| } | ||
| return { type: 'unknown', default: undefined } | ||
| } | ||
| const schemaObj = schema as OpenAPIV3_1.SchemaObject | ||
| // Handle allOf, anyOf, oneOf | ||
| if (schemaObj.allOf) { | ||
| const types = schemaObj.allOf.map((s) => | ||
| getTypeFromSchema(s, document, options), | ||
| ) | ||
| return { | ||
| type: | ||
| types.length > 0 | ||
| ? types.map((t) => formatTypeValue(t.type)).join(' & ') | ||
| : 'unknown', | ||
| default: schemaObj.default, | ||
| } | ||
| } | ||
| if (schemaObj.anyOf || schemaObj.oneOf) { | ||
| const types = (schemaObj.anyOf || schemaObj.oneOf || []).map((s) => | ||
| getTypeFromSchema(s, document, options), | ||
| ) | ||
| return { | ||
| type: | ||
| types.length > 0 | ||
| ? `(${types.map((t) => formatTypeValue(t.type)).join(' | ')})` | ||
| : 'unknown', | ||
| default: schemaObj.default, | ||
| } | ||
| } | ||
| // Handle enum | ||
| if (schemaObj.enum) { | ||
| return { | ||
| type: schemaObj.enum.map((v) => `"${String(v)}"`).join(' | '), | ||
| default: schemaObj.default, | ||
| } | ||
| } | ||
| // Handle primitive types | ||
| if (schemaObj.type === 'string') { | ||
| if (schemaObj.format === 'date' || schemaObj.format === 'date-time') { | ||
| return { type: 'string', default: schemaObj.default } | ||
| } | ||
| return { type: 'string', default: schemaObj.default } | ||
| } | ||
| if (schemaObj.type === 'number' || schemaObj.type === 'integer') { | ||
| return { type: 'number', default: schemaObj.default } | ||
| } | ||
| if (schemaObj.type === 'boolean') { | ||
| return { type: 'boolean', default: schemaObj.default } | ||
| } | ||
| // Handle array | ||
| if (schemaObj.type === 'array') { | ||
| const items = schemaObj.items | ||
| if (items) { | ||
| const itemType = getTypeFromSchema(items, document, options) | ||
| return { | ||
| type: `Array<${formatTypeValue(itemType.type)}>`, | ||
| default: schemaObj.default, | ||
| } | ||
| } | ||
| return { type: 'unknown[]', default: schemaObj.default } | ||
| } | ||
| // Handle object | ||
| if (schemaObj.type === 'object' || schemaObj.properties) { | ||
| const props: Record<string, { type: unknown; default?: unknown }> = {} | ||
| const required = schemaObj.required || [] | ||
| if (schemaObj.properties) { | ||
| for (const [key, value] of Object.entries(schemaObj.properties)) { | ||
| const propType = getTypeFromSchema(value, document, options) | ||
| // Check if property has default value | ||
| // Need to resolve $ref if present to check for default | ||
| let hasDefault = false | ||
| if ('$ref' in value) { | ||
| const resolved = resolveSchemaRef(value.$ref, document) | ||
| if (resolved) { | ||
| hasDefault = resolved.default !== undefined | ||
| } | ||
| } else { | ||
| const propSchema = value as OpenAPIV3_1.SchemaObject | ||
| hasDefault = propSchema.default !== undefined | ||
| } | ||
| const isInRequired = required.includes(key) | ||
| // If defaultNonNullable is true and has default, treat as required | ||
| // Otherwise, mark as optional if not in required array | ||
| if (defaultNonNullable && hasDefault && !isInRequired) { | ||
| props[key] = propType | ||
| } else if (!isInRequired) { | ||
| props[`${key}?`] = propType | ||
| } else { | ||
| props[key] = propType | ||
| } | ||
| } | ||
| } | ||
| // Handle additionalProperties | ||
| if (schemaObj.additionalProperties) { | ||
| if (schemaObj.additionalProperties === true) { | ||
| props['[key: string]'] = { type: 'unknown', default: undefined } | ||
| } else if (typeof schemaObj.additionalProperties === 'object') { | ||
| const additionalType = getTypeFromSchema( | ||
| schemaObj.additionalProperties, | ||
| document, | ||
| options, | ||
| ) | ||
| props['[key: string]'] = { | ||
| type: additionalType.type, | ||
| default: additionalType.default, | ||
| } | ||
| } | ||
| } | ||
| return { type: { ...props }, default: schemaObj.default } | ||
| } | ||
| // Handle oneOf/anyOf already handled above, but check again for safety | ||
| return { type: 'unknown', default: undefined } | ||
| } | ||
| /** | ||
| * Check if a value is a ParameterDefinition | ||
| */ | ||
| function isParameterDefinition(value: unknown): value is ParameterDefinition { | ||
| return ( | ||
| typeof value === 'object' && | ||
| value !== null && | ||
| 'type' in value && | ||
| 'in' in value && | ||
| 'name' in value | ||
| ) | ||
| } | ||
| /** | ||
| * Check if all properties in an object are optional | ||
| */ | ||
| export function areAllPropertiesOptional( | ||
| obj: Record<string, unknown>, | ||
| ): boolean { | ||
| const entries = Object.entries(obj) | ||
| if (entries.length === 0) { | ||
| return true | ||
| } | ||
| return entries.every(([key, value]) => { | ||
| // If key ends with '?', it's optional (from getTypeFromSchema) | ||
| if (key.endsWith('?')) { | ||
| return true | ||
| } | ||
| // If it's a ParameterDefinition, check required field | ||
| if (isParameterDefinition(value)) { | ||
| return value.required === false | ||
| } | ||
| // If it's a type object, check if the type itself is optional | ||
| if (isTypeObject(value)) { | ||
| // For type objects, check if the type is an object with all optional properties | ||
| if ( | ||
| typeof value.type === 'object' && | ||
| value.type !== null && | ||
| !Array.isArray(value.type) | ||
| ) { | ||
| return areAllPropertiesOptional(value.type as Record<string, unknown>) | ||
| } | ||
| return false | ||
| } | ||
| // For nested objects, recursively check | ||
| if (typeof value === 'object' && value !== null && !Array.isArray(value)) { | ||
| return areAllPropertiesOptional(value as Record<string, unknown>) | ||
| } | ||
| return false | ||
| }) | ||
| } | ||
| /** | ||
| * Format a type object to TypeScript interface/type string | ||
| */ | ||
| export function formatType( | ||
| obj: Record<string, unknown>, | ||
| indent: number = 0, | ||
| ): string { | ||
| const indentStr = ' '.repeat(indent) | ||
| const nextIndent = indent + 1 | ||
| const nextIndentStr = ' '.repeat(nextIndent) | ||
| const entries = Object.entries(obj) | ||
| .map(([key, value]) => { | ||
| // Handle string values (e.g., component references) | ||
| if (typeof value === 'string') { | ||
| return `${nextIndentStr}${key}: ${value}` | ||
| } | ||
| // Handle ParameterDefinition for params and query | ||
| if (isParameterDefinition(value)) { | ||
| const typeStr = formatTypeValue(value.type, nextIndent) | ||
| const isOptional = value.required === false | ||
| const keyWithOptional = isOptional ? `${key}?` : key | ||
| let description = '' | ||
| if (value.description) { | ||
| description += `${nextIndentStr}/**\n${nextIndentStr} * ${value.description}` | ||
| if (typeof value.default !== 'undefined') { | ||
| description += `\n${nextIndentStr} * @default {${value.default}}` | ||
| } | ||
| description = `${description}\n${nextIndentStr} */\n${nextIndentStr}` | ||
| } else if (typeof value.default !== 'undefined') { | ||
| description += `${nextIndentStr}/** @default {${value.default}} */\n${nextIndentStr}` | ||
| } else { | ||
| description = nextIndentStr | ||
| } | ||
| return `${description}${keyWithOptional}: ${typeStr}` | ||
| } | ||
| // Handle { type: unknown, default?: unknown } structure (from getTypeFromSchema) | ||
| if (isTypeObject(value)) { | ||
| const formattedValue = formatTypeValue(value.type, nextIndent) | ||
| // Key already has '?' if it's optional (from getTypeFromSchema), keep it as is | ||
| return `${nextIndentStr}${key}: ${formattedValue}` | ||
| } | ||
| // Check if value is an object (like params, query) with all optional properties | ||
| const valueAllOptional = | ||
| typeof value === 'object' && | ||
| value !== null && | ||
| !Array.isArray(value) && | ||
| areAllPropertiesOptional(value as Record<string, unknown>) | ||
| const optionalMarker = valueAllOptional ? '?' : '' | ||
| const formattedValue = formatTypeValue(value, nextIndent) | ||
| return `${nextIndentStr}${key}${optionalMarker}: ${formattedValue}` | ||
| }) | ||
| .join(';\n') | ||
| if (entries.length === 0) { | ||
| return '{}' | ||
| } | ||
| return `{\n${entries};\n${indentStr}}` | ||
| } | ||
| /** | ||
| * Check if a value is a type object with { type, default? } structure | ||
| */ | ||
| function isTypeObject( | ||
| value: unknown, | ||
| ): value is { type: unknown; default?: unknown } { | ||
| return ( | ||
| typeof value === 'object' && | ||
| value !== null && | ||
| 'type' in value && | ||
| Object.keys(value).length <= 2 && | ||
| (!('default' in value) || Object.keys(value).length === 2) | ||
| ) | ||
| } | ||
| /** | ||
| * Format a type value to TypeScript type string | ||
| */ | ||
| export function formatTypeValue(value: unknown, indent: number = 0): string { | ||
| if (typeof value === 'string') { | ||
| return value | ||
| } | ||
| // Handle { type: unknown, default?: unknown } structure | ||
| if (isTypeObject(value)) { | ||
| return formatTypeValue(value.type, indent) | ||
| } | ||
| if (typeof value === 'object' && value !== null && !Array.isArray(value)) { | ||
| return formatType(value as Record<string, unknown>, indent) | ||
| } | ||
| return String(value) | ||
| } | ||
| /** | ||
| * Extract parameters from OpenAPI operation | ||
| */ | ||
| export function extractParameters( | ||
| pathItem: OpenAPIV3_1.PathItemObject | undefined, | ||
| operation: OpenAPIV3_1.OperationObject | undefined, | ||
| document: OpenAPIV3_1.Document, | ||
| ): { | ||
| pathParams: Record<string, ParameterDefinition> | ||
| queryParams: Record<string, ParameterDefinition> | ||
| headerParams: Record<string, ParameterDefinition> | ||
| } { | ||
| const pathParams: Record<string, ParameterDefinition> = {} | ||
| const queryParams: Record<string, ParameterDefinition> = {} | ||
| const headerParams: Record<string, ParameterDefinition> = {} | ||
| const allParams = [ | ||
| ...(pathItem?.parameters || []), | ||
| ...(operation?.parameters || []), | ||
| ] | ||
| for (const param of allParams) { | ||
| if ('$ref' in param) { | ||
| // Resolve $ref parameter | ||
| const resolved = resolveParameterRef(param.$ref, document) | ||
| if ( | ||
| resolved && | ||
| 'in' in resolved && | ||
| 'name' in resolved && | ||
| typeof resolved.in === 'string' && | ||
| typeof resolved.name === 'string' | ||
| ) { | ||
| const paramSchema = | ||
| 'schema' in resolved && resolved.schema ? resolved.schema : {} | ||
| const { type: paramType, default: paramDefault } = getTypeFromSchema( | ||
| paramSchema, | ||
| document, | ||
| { defaultNonNullable: false }, | ||
| ) | ||
| const result = { | ||
| ...resolved, | ||
| type: paramType, | ||
| default: paramDefault, | ||
| } | ||
| if (resolved.in === 'path') { | ||
| pathParams[resolved.name] = result | ||
| } else if (resolved.in === 'query') { | ||
| queryParams[resolved.name] = result | ||
| } else if (resolved.in === 'header') { | ||
| headerParams[resolved.name] = result | ||
| } | ||
| } | ||
| continue | ||
| } | ||
| const paramSchema = param.schema || {} | ||
| const { type: paramType, default: paramDefault } = getTypeFromSchema( | ||
| paramSchema, | ||
| document, | ||
| { defaultNonNullable: false }, | ||
| ) | ||
| const result = { | ||
| ...param, | ||
| type: paramType, | ||
| default: paramDefault, | ||
| } | ||
| if (param.in === 'path') { | ||
| pathParams[param.name] = result | ||
| } else if (param.in === 'query') { | ||
| queryParams[param.name] = result | ||
| } else if (param.in === 'header') { | ||
| headerParams[param.name] = result | ||
| } | ||
| } | ||
| return { pathParams, queryParams, headerParams } | ||
| } | ||
| /** | ||
| * Extract request body from OpenAPI operation | ||
| */ | ||
| export function extractRequestBody( | ||
| requestBody: | ||
| | OpenAPIV3_1.RequestBodyObject | ||
| | OpenAPIV3_1.ReferenceObject | ||
| | undefined, | ||
| document: OpenAPIV3_1.Document, | ||
| ): unknown { | ||
| if (!requestBody) { | ||
| return undefined | ||
| } | ||
| if ('$ref' in requestBody) { | ||
| const resolved = resolveSchemaRef(requestBody.$ref, document) | ||
| if (resolved && 'content' in resolved && resolved.content) { | ||
| const content = | ||
| resolved.content as OpenAPIV3_1.RequestBodyObject['content'] | ||
| const jsonContent = content['application/json'] | ||
| if (jsonContent && 'schema' in jsonContent && jsonContent.schema) { | ||
| return getTypeFromSchema(jsonContent.schema, document, { | ||
| defaultNonNullable: false, | ||
| }).type | ||
| } | ||
| } | ||
| return 'unknown' | ||
| } | ||
| const content = requestBody.content | ||
| if (content) { | ||
| const jsonContent = content['application/json'] | ||
| if (jsonContent && 'schema' in jsonContent && jsonContent.schema) { | ||
| return getTypeFromSchema(jsonContent.schema, document, { | ||
| defaultNonNullable: false, | ||
| }).type | ||
| } | ||
| } | ||
| return undefined | ||
| } |
| export * from './create-url-map' | ||
| export * from './generate-interface' |
| export function wrapInterfaceKeyGuard(key: string): string { | ||
| if (key.includes('/')) { | ||
| return `[\`${key}\`]` | ||
| } | ||
| return key | ||
| } |
| { | ||
| "compilerOptions": { | ||
| // Environment setup & latest features | ||
| "lib": ["ESNext"], | ||
| "target": "ESNext", | ||
| "module": "Preserve", | ||
| "moduleDetection": "force", | ||
| "jsx": "react-jsx", | ||
| "allowJs": true, | ||
| // Bundler mode | ||
| "moduleResolution": "bundler", | ||
| "verbatimModuleSyntax": true, | ||
| "emitDeclarationOnly": true, | ||
| // Best practices | ||
| "strict": true, | ||
| "skipLibCheck": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "noUncheckedIndexedAccess": true, | ||
| "noImplicitOverride": true, | ||
| // Some stricter flags (disabled by default) | ||
| "noUnusedLocals": false, | ||
| "noUnusedParameters": false, | ||
| "noPropertyAccessFromIndexSignature": false, | ||
| "declaration": true, | ||
| "declarationMap": true, | ||
| "outDir": "dist", | ||
| "rootDir": "src" | ||
| }, | ||
| "include": ["src/**/*.ts"], | ||
| "exclude": ["dist", "node_modules"] | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No License Found
LicenseLicense information could not be found.
Found 1 instance in 1 package
0
-100%29624
-64.31%24
-31.43%201
-88.31%1
Infinity%+ Added
+ Added
- Removed
- Removed
Updated
Updated