fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3242 from chrg1001/fix/refs-broken

fix: inline deep path $ref references

authored by

Lubos and committed by
GitHub
4999aad1 f71f0b82

+313 -9
+5
.changeset/cuddly-jokes-walk.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **parser**: inline deep path `$ref` references
+137
packages/openapi-ts/src/__tests__/index.test.ts
··· 5 5 type Config = Parameters<typeof createClient>[0]; 6 6 7 7 describe('createClient', () => { 8 + it('handles deep path $ref without errors', async () => { 9 + // This test verifies that deep path refs like 10 + // #/components/schemas/Foo/properties/bar/items are inlined 11 + // instead of being treated as symbol references (which would fail) 12 + const config: Config = { 13 + dryRun: true, 14 + input: { 15 + components: { 16 + schemas: { 17 + Bar: { 18 + properties: { 19 + nested: { 20 + // Deep path ref - should be inlined, not treated as symbol 21 + $ref: '#/components/schemas/Foo/properties/items/items', 22 + }, 23 + }, 24 + type: 'object', 25 + }, 26 + Foo: { 27 + properties: { 28 + items: { 29 + items: { 30 + properties: { 31 + name: { type: 'string' }, 32 + }, 33 + type: 'object', 34 + }, 35 + type: 'array', 36 + }, 37 + }, 38 + type: 'object', 39 + }, 40 + }, 41 + }, 42 + info: { title: 'deep-ref-test', version: '1.0.0' }, 43 + openapi: '3.1.0', 44 + }, 45 + logs: { 46 + level: 'silent', 47 + }, 48 + output: 'output', 49 + plugins: ['@hey-api/typescript'], 50 + }; 51 + 52 + // Should not throw "Symbol finalName has not been resolved yet" error 53 + const results = await createClient(config); 54 + expect(results).toHaveLength(1); 55 + }); 56 + 57 + it('handles deep path $ref in OpenAPI 3.0.x without errors', async () => { 58 + const config: Config = { 59 + dryRun: true, 60 + input: { 61 + components: { 62 + schemas: { 63 + Bar: { 64 + properties: { 65 + nested: { 66 + $ref: '#/components/schemas/Foo/properties/items/items', 67 + }, 68 + }, 69 + type: 'object', 70 + }, 71 + Foo: { 72 + properties: { 73 + items: { 74 + items: { 75 + properties: { 76 + name: { type: 'string' }, 77 + }, 78 + type: 'object', 79 + }, 80 + type: 'array', 81 + }, 82 + }, 83 + type: 'object', 84 + }, 85 + }, 86 + }, 87 + info: { title: 'deep-ref-test', version: '1.0.0' }, 88 + openapi: '3.0.0', 89 + paths: {}, 90 + }, 91 + logs: { 92 + level: 'silent', 93 + }, 94 + output: 'output', 95 + plugins: ['@hey-api/typescript'], 96 + }; 97 + 98 + const results = await createClient(config); 99 + expect(results).toHaveLength(1); 100 + }); 101 + 102 + it('handles deep path $ref in OpenAPI 2.0 (Swagger) without errors', async () => { 103 + const config: Config = { 104 + dryRun: true, 105 + input: { 106 + definitions: { 107 + Bar: { 108 + properties: { 109 + nested: { 110 + $ref: '#/definitions/Foo/properties/items/items', 111 + }, 112 + }, 113 + type: 'object', 114 + }, 115 + Foo: { 116 + properties: { 117 + items: { 118 + items: { 119 + properties: { 120 + name: { type: 'string' }, 121 + }, 122 + type: 'object', 123 + }, 124 + type: 'array', 125 + }, 126 + }, 127 + type: 'object', 128 + }, 129 + }, 130 + info: { title: 'deep-ref-test', version: '1.0.0' }, 131 + paths: {}, 132 + swagger: '2.0', 133 + }, 134 + logs: { 135 + level: 'silent', 136 + }, 137 + output: 'output', 138 + plugins: ['@hey-api/typescript'], 139 + }; 140 + 141 + const results = await createClient(config); 142 + expect(results).toHaveLength(1); 143 + }); 144 + 8 145 it('1 config, 1 input, 1 output', async () => { 9 146 const config: Config = { 10 147 dryRun: true,
+4 -3
packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '~/openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; 10 - import { refToName } from '~/utils/ref'; 10 + import { isTopLevelComponentRef, refToName } from '~/utils/ref'; 11 11 12 12 import type { SchemaObject } from '../types/spec'; 13 13 ··· 554 554 state: SchemaState; 555 555 }): IR.SchemaObject => { 556 556 const irSchema: IR.SchemaObject = {}; 557 - // Inline non-component refs (e.g. #/paths/...) to avoid generating orphaned named types 558 - const isComponentsRef = schema.$ref.startsWith('#/definitions/'); 557 + // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/definitions/Foo/properties/bar) 558 + // to avoid generating orphaned named types or referencing unregistered symbols 559 + const isComponentsRef = isTopLevelComponentRef(schema.$ref); 559 560 if (!isComponentsRef) { 560 561 if (!state.circularReferenceTracker.has(schema.$ref)) { 561 562 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+4 -3
packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '~/openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; 10 - import { refToName } from '~/utils/ref'; 10 + import { isTopLevelComponentRef, refToName } from '~/utils/ref'; 11 11 12 12 import type { ReferenceObject, SchemaObject } from '../types/spec'; 13 13 ··· 976 976 schema: ReferenceObject; 977 977 state: SchemaState; 978 978 }): IR.SchemaObject => { 979 - // Inline non-component refs (e.g. #/paths/...) to avoid generating orphaned named types 980 - const isComponentsRef = schema.$ref.startsWith('#/components/'); 979 + // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/components/schemas/Foo/properties/bar) 980 + // to avoid generating orphaned named types or referencing unregistered symbols 981 + const isComponentsRef = isTopLevelComponentRef(schema.$ref); 981 982 if (!isComponentsRef) { 982 983 if (!state.circularReferenceTracker.has(schema.$ref)) { 983 984 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+4 -3
packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts
··· 7 7 SchemaWithRequired, 8 8 } from '~/openApi/shared/types/schema'; 9 9 import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; 10 - import { refToName } from '~/utils/ref'; 10 + import { isTopLevelComponentRef, refToName } from '~/utils/ref'; 11 11 12 12 import type { SchemaObject } from '../types/spec'; 13 13 ··· 1037 1037 schema: SchemaWithRequired<SchemaObject, '$ref'>; 1038 1038 state: SchemaState; 1039 1039 }): IR.SchemaObject => { 1040 - // Inline non-component refs (e.g. #/paths/...) to avoid generating orphaned named types 1041 - const isComponentsRef = schema.$ref.startsWith('#/components/'); 1040 + // Inline non-component refs (e.g. #/paths/...) and deep path refs (e.g. #/components/schemas/Foo/properties/bar) 1041 + // to avoid generating orphaned named types or referencing unregistered symbols 1042 + const isComponentsRef = isTopLevelComponentRef(schema.$ref); 1042 1043 if (!isComponentsRef) { 1043 1044 if (!state.circularReferenceTracker.has(schema.$ref)) { 1044 1045 const refSchema = context.resolveRef<SchemaObject>(schema.$ref);
+91
packages/openapi-ts/src/utils/__tests__/ref.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import { 4 + isTopLevelComponentRef, 5 + jsonPointerToPath, 6 + pathToJsonPointer, 7 + } from '../ref'; 8 + 9 + describe('jsonPointerToPath', () => { 10 + it('parses root pointer', () => { 11 + expect(jsonPointerToPath('#')).toEqual([]); 12 + expect(jsonPointerToPath('')).toEqual([]); 13 + }); 14 + 15 + it('parses component ref', () => { 16 + expect(jsonPointerToPath('#/components/schemas/Foo')).toEqual([ 17 + 'components', 18 + 'schemas', 19 + 'Foo', 20 + ]); 21 + }); 22 + 23 + it('parses deep path ref', () => { 24 + expect( 25 + jsonPointerToPath('#/components/schemas/Foo/properties/bar/items'), 26 + ).toEqual(['components', 'schemas', 'Foo', 'properties', 'bar', 'items']); 27 + }); 28 + }); 29 + 30 + describe('pathToJsonPointer', () => { 31 + it('converts empty path to root pointer', () => { 32 + expect(pathToJsonPointer([])).toBe('#'); 33 + }); 34 + 35 + it('converts path to pointer', () => { 36 + expect(pathToJsonPointer(['components', 'schemas', 'Foo'])).toBe( 37 + '#/components/schemas/Foo', 38 + ); 39 + }); 40 + }); 41 + 42 + describe('isTopLevelComponentRef', () => { 43 + describe('OpenAPI 3.x refs', () => { 44 + it('returns true for top-level component refs', () => { 45 + expect(isTopLevelComponentRef('#/components/schemas/Foo')).toBe(true); 46 + expect(isTopLevelComponentRef('#/components/parameters/Bar')).toBe(true); 47 + expect(isTopLevelComponentRef('#/components/responses/Error')).toBe(true); 48 + expect(isTopLevelComponentRef('#/components/requestBodies/Body')).toBe( 49 + true, 50 + ); 51 + }); 52 + 53 + it('returns false for deep path refs', () => { 54 + expect( 55 + isTopLevelComponentRef('#/components/schemas/Foo/properties/bar'), 56 + ).toBe(false); 57 + expect( 58 + isTopLevelComponentRef('#/components/schemas/Foo/properties/bar/items'), 59 + ).toBe(false); 60 + expect(isTopLevelComponentRef('#/components/schemas/Foo/allOf/0')).toBe( 61 + false, 62 + ); 63 + }); 64 + }); 65 + 66 + describe('OpenAPI 2.0 refs', () => { 67 + it('returns true for top-level definitions refs', () => { 68 + expect(isTopLevelComponentRef('#/definitions/Foo')).toBe(true); 69 + expect(isTopLevelComponentRef('#/definitions/Bar')).toBe(true); 70 + }); 71 + 72 + it('returns false for deep path refs', () => { 73 + expect(isTopLevelComponentRef('#/definitions/Foo/properties/bar')).toBe( 74 + false, 75 + ); 76 + expect( 77 + isTopLevelComponentRef('#/definitions/Foo/properties/bar/items'), 78 + ).toBe(false); 79 + }); 80 + }); 81 + 82 + describe('non-component refs', () => { 83 + it('returns false for path refs', () => { 84 + expect(isTopLevelComponentRef('#/paths/~1users/get')).toBe(false); 85 + }); 86 + 87 + it('returns false for other refs', () => { 88 + expect(isTopLevelComponentRef('#/info/title')).toBe(false); 89 + }); 90 + }); 91 + });
+29
packages/openapi-ts/src/utils/ref.ts
··· 94 94 return '#' + (segments ? `/${segments}` : ''); 95 95 }; 96 96 97 + /** 98 + * Checks if a $ref points to a top-level component (not a deep path reference). 99 + * 100 + * Top-level component references: 101 + * - OpenAPI 3.x: #/components/{type}/{name} (3 segments) 102 + * - OpenAPI 2.0: #/definitions/{name} (2 segments) 103 + * 104 + * Deep path references (4+ segments for 3.x, 3+ for 2.0) should be inlined 105 + * because they don't have corresponding registered symbols. 106 + * 107 + * @param $ref - The $ref string to check 108 + * @returns true if the ref points to a top-level component, false otherwise 109 + */ 110 + export const isTopLevelComponentRef = ($ref: string): boolean => { 111 + const path = jsonPointerToPath($ref); 112 + 113 + // OpenAPI 3.x: #/components/{type}/{name} = 3 segments 114 + if (path[0] === 'components') { 115 + return path.length === 3; 116 + } 117 + 118 + // OpenAPI 2.0: #/definitions/{name} = 2 segments 119 + if (path[0] === 'definitions') { 120 + return path.length === 2; 121 + } 122 + 123 + return false; 124 + }; 125 + 97 126 export const resolveRef = <T>({ 98 127 $ref, 99 128 spec,
+39
specs/3.0.x/deep-path-ref.yaml
··· 1 + openapi: 3.0.1 2 + info: 3 + title: Deep Path Ref Test 4 + description: Test case for deep path references in #/paths/... 5 + version: '1' 6 + paths: 7 + /users: 8 + get: 9 + operationId: getUsers 10 + responses: 11 + '200': 12 + description: Success 13 + content: 14 + application/json: 15 + schema: 16 + type: array 17 + items: 18 + type: object 19 + properties: 20 + id: 21 + type: integer 22 + name: 23 + type: string 24 + /posts: 25 + get: 26 + operationId: getPosts 27 + responses: 28 + '200': 29 + description: Success 30 + content: 31 + application/json: 32 + schema: 33 + type: object 34 + properties: 35 + author: 36 + # Deep path reference to /users response schema item 37 + $ref: '#/paths/~1users/get/responses/200/content/application~1json/schema/items' 38 + title: 39 + type: string