diff --git a/packages/schema-sdk/src/openapi.ts b/packages/schema-sdk/src/openapi.ts index 643b75b..ac48eb8 100644 --- a/packages/schema-sdk/src/openapi.ts +++ b/packages/schema-sdk/src/openapi.ts @@ -12,7 +12,7 @@ import { Response, } from './openapi.types'; import { OpenAPIOptions } from './types'; -import { createPathTemplateLiteral, applyJsonPatch } from './utils'; +import { createPathTemplateLiteral, applyJsonPatch, resolveMaybeRef } from './utils'; /** includes: { @@ -106,12 +106,13 @@ export const getApiTypeNameSafe = ( export const getOperationReturnType = ( options: OpenAPIOptions, + schema: OpenAPISpec, operation: Operation, method: string ) => { if (operation.responses) { if (operation.responses['200']) { - const prop = operation.responses['200']; + const prop = resolveMaybeRef(schema, operation.responses['200'] as any); return getResponseType(options, prop); } } @@ -218,42 +219,26 @@ const initParams = (): ParameterInterfaces => { }; }; -// Resolve a parameter $ref against the spec's global parameters collection -function resolveParameterRef( - schema: OpenAPISpec, - param: any -): Parameter { - if (param && typeof param === 'object' && (param as any).$ref) { - const ref: string = (param as any).$ref; - if (ref.includes('/parameters/')) { - const parts = ref.split('/'); - const name = parts[parts.length - 1]; - const resolved = schema.parameters?.[name]; - if (resolved) return resolved as Parameter; - } - } - return param as Parameter; -} - export function generateOpenApiParams( options: OpenAPIOptions, schema: OpenAPISpec, path: string, pathItem: OpenAPIPathItem ): t.TSInterfaceDeclaration[] { + const resolvedPathItem = resolveMaybeRef(schema, pathItem as any); const opParams: OpParameterInterfaces = getOpenApiParams( options, schema, path, - pathItem + resolvedPathItem ); const interfaces: t.TSInterfaceDeclaration[] = []; ['get', 'post', 'put', 'delete', 'options', 'head', 'patch'].forEach( (method) => { - if (Object.prototype.hasOwnProperty.call(pathItem, method)) { + if (Object.prototype.hasOwnProperty.call(resolvedPathItem, method)) { // @ts-ignore - const operation: Operation = pathItem[method]; - if (!shouldIncludeOperation(options, pathItem, path, method as any)) + const operation: Operation = (resolvedPathItem as any)[method]; + if (!shouldIncludeOperation(options, resolvedPathItem, path, method as any)) return; // @ts-ignore @@ -352,7 +337,7 @@ export function getOpenApiParams( // BEGIN SANITIZE PARAMS pathItem.parameters = pathItem.parameters ?? []; const resolvedPathParams = pathItem.parameters.map((p) => - resolveParameterRef(schema, p as any) + resolveMaybeRef(schema, p as any) ); const pathParms = resolvedPathParams.filter((param) => param.in === 'path') ?? []; if (pathParms.length !== pathInfo.params.length) { @@ -377,7 +362,7 @@ export function getOpenApiParams( // load Path-Level params pathItem.parameters.forEach((param) => { - const resolved = resolveParameterRef(schema, param as any); + const resolved = resolveMaybeRef(schema, param as any); opParams.pathLevel[resolved.in].push(resolved); }); @@ -401,7 +386,7 @@ export function getOpenApiParams( if (operation.parameters) { // Categorize parameters by 'in' field operation.parameters.forEach((param) => { - const resolved = resolveParameterRef(schema, param as any); + const resolved = resolveMaybeRef(schema, param as any); opParamMethod[resolved.in].push(resolved); }); } @@ -463,6 +448,7 @@ const getOperationTypeName = ( export const createOperation = ( options: OpenAPIOptions, + schema: OpenAPISpec, operation: Operation, path: string, method: string, @@ -493,7 +479,7 @@ export const createOperation = ( (param) => param.in === 'query' ); - const returnType = getOperationReturnType(options, operation, method); + const returnType = getOperationReturnType(options, schema, operation, method); const methodName = getOperationMethodName(options, operation, method, path); const callMethod = t.callExpression( @@ -559,10 +545,10 @@ export function generateMethods( if (alias) { methods.push( - createOperation(options, operation, path, method, alias) + createOperation(options, schema, operation, path, method, alias) ); } - methods.push(createOperation(options, operation, path, method)); + methods.push(createOperation(options, schema, operation, path, method)); }); }); @@ -709,7 +695,7 @@ export function collectReactQueryHookComponents( ), ]); const requestTypeName = getOperationTypeName(options, operation, method, path) + 'Request'; - const returnTypeAST = getOperationReturnType(options, operation, method); + const returnTypeAST = getOperationReturnType(options, schema, operation, method); const methodName = opMethodName; const importDecls: t.ImportDeclaration[] = [ diff --git a/packages/schema-sdk/src/utils.ts b/packages/schema-sdk/src/utils.ts index be056cb..b668a86 100644 --- a/packages/schema-sdk/src/utils.ts +++ b/packages/schema-sdk/src/utils.ts @@ -2,6 +2,7 @@ import * as t from '@babel/types'; import * as jsonpatch from 'fast-json-patch'; import { OpenAPIOptions } from './types'; +import type { OpenAPISpec } from './openapi.types'; /** * Converts a URL path with placeholders into a Babel AST TemplateLiteral. @@ -84,3 +85,48 @@ export function applyJsonPatch(spec: T, options: OpenAPIOptions): T { throw new Error(`Failed to apply JSON patches: ${error instanceof Error ? error.message : String(error)}`); } } + +/** + * Resolve an in-spec $ref object against OpenAPI v2 sections (definitions, parameters, responses). + * If the provided object is not a $ref, it is returned as-is. + */ +export function resolveRefObject(spec: OpenAPISpec, obj: any): T { + let current: any = obj; + let depth = 0; + const MAX_DEPTH = 8; + while (current && typeof current === 'object' && current.$ref && depth < MAX_DEPTH) { + const ref: string = current.$ref as string; + if (!ref.startsWith('#/')) break; + const parts = ref.slice(2).split('/'); // remove leading '#/' + const section = parts[0]; + const key = decodeURIComponent(parts.slice(1).join('/')); + let resolved: any; + switch (section) { + case 'definitions': + resolved = spec.definitions?.[key]; + break; + case 'parameters': + resolved = spec.parameters?.[key]; + break; + case 'responses': + resolved = spec.responses?.[key]; + break; + default: + resolved = undefined; + } + if (!resolved) break; + current = resolved; + depth += 1; + } + return current as T; +} + +/** + * Convenience wrapper to resolve an object if it is a $ref, otherwise return it unchanged. + */ +export function resolveMaybeRef(spec: OpenAPISpec, obj: any): T { + if (obj && typeof obj === 'object' && (obj as any).$ref) { + return resolveRefObject(spec, obj); + } + return obj as T; +}