Skip to content

Commit ee01ca7

Browse files
authored
fix: decode URI-encoded names in getDependencies to fix missing filtered operations
Agent-Logs-Url: https://github.com/hey-api/openapi-ts/sessions/11f92a24-26b1-4ed3-8ea0-b3a4775a2eeb
1 parent bfcfc2b commit ee01ca7

2 files changed

Lines changed: 190 additions & 1 deletion

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type { Logger } from '@hey-api/codegen-core';
2+
3+
import { createFilteredDependencies, type Filters } from '../../utils/filter';
4+
import { buildGraph } from '../../utils/graph';
5+
import { buildResourceMetadata } from '../meta';
6+
7+
const loggerStub = {
8+
timeEvent: () => ({ timeEnd: () => {} }),
9+
} as unknown as Logger;
10+
11+
function createFilters(): Filters {
12+
return {
13+
deprecated: true,
14+
operations: {
15+
exclude: new Set(),
16+
include: new Set(),
17+
},
18+
orphans: false,
19+
parameters: {
20+
exclude: new Set(),
21+
include: new Set(),
22+
},
23+
preserveOrder: false,
24+
requestBodies: {
25+
exclude: new Set(),
26+
include: new Set(),
27+
},
28+
responses: {
29+
exclude: new Set(),
30+
include: new Set(),
31+
},
32+
schemas: {
33+
exclude: new Set(),
34+
include: new Set(),
35+
},
36+
tags: {
37+
exclude: new Set(),
38+
include: new Set(),
39+
},
40+
};
41+
}
42+
43+
describe('buildResourceMetadata', () => {
44+
it('decodes URI-encoded names in operation dependencies', () => {
45+
// Simulate a spec where the $ref value has been URL-encoded by the JSON schema
46+
// ref-parser (e.g. angle brackets in generic schema names: Foo<Bar> -> Foo%3CBar%3E).
47+
// The schema key in components/schemas remains unencoded (Foo<Bar>), but the $ref
48+
// pointing to it is URL-encoded. Without the fix, the dependency key would be
49+
// 'schema/Foo%3CBar%3E' which does not match 'schema/Foo<Bar>' in resourceMetadata.schemas.
50+
const spec = {
51+
components: {
52+
schemas: {
53+
ClientItem: {
54+
properties: { id: { type: 'string' } },
55+
type: 'object',
56+
},
57+
// Key uses literal angle brackets (as seen when traversing the spec object)
58+
'PaginatedList<ClientItem>': {
59+
properties: {
60+
items: {
61+
items: { $ref: '#/components/schemas/ClientItem' },
62+
type: 'array',
63+
},
64+
},
65+
type: 'object',
66+
},
67+
},
68+
},
69+
paths: {
70+
'/api/items': {
71+
get: {
72+
// $ref uses URL-encoded form as the JSON schema ref-parser would produce
73+
responses: {
74+
'200': {
75+
content: {
76+
'application/json': {
77+
schema: { $ref: '#/components/schemas/PaginatedList%3CClientItem%3E' },
78+
},
79+
},
80+
description: '',
81+
},
82+
},
83+
tags: ['Client'],
84+
},
85+
},
86+
'/api/other': {
87+
get: {
88+
responses: { '200': { description: '' } },
89+
tags: ['Other'],
90+
},
91+
},
92+
},
93+
};
94+
95+
const { graph } = buildGraph(spec, loggerStub);
96+
const { resourceMetadata } = buildResourceMetadata(graph, loggerStub);
97+
98+
// The operation's dependencies should use the decoded name so they match
99+
// the keys in resourceMetadata.schemas
100+
const opDeps =
101+
resourceMetadata.operations.get('operation/GET /api/items')?.dependencies ?? new Set();
102+
expect(opDeps.has('schema/PaginatedList<ClientItem>')).toBe(true);
103+
expect(opDeps.has('schema/PaginatedList%3CClientItem%3E')).toBe(false);
104+
});
105+
106+
it('includes operations with URL-encoded schema references when filtering by tag', () => {
107+
// Reproduces the issue: when using tag filters, operations whose response schemas
108+
// have names with URL-unsafe characters (e.g. angle brackets for generic types like
109+
// PaginatedListItem<T>) were incorrectly dropped because the URL-encoded $ref
110+
// produced by the JSON schema ref-parser did not match the unencoded schema key
111+
// in resourceMetadata.schemas.
112+
const spec = {
113+
components: {
114+
schemas: {
115+
ClientItem: {
116+
properties: { id: { type: 'string' } },
117+
type: 'object',
118+
},
119+
OtherResponse: {
120+
properties: { name: { type: 'string' } },
121+
type: 'object',
122+
},
123+
'PaginatedList<ClientItem>': {
124+
properties: {
125+
items: {
126+
items: { $ref: '#/components/schemas/ClientItem' },
127+
type: 'array',
128+
},
129+
},
130+
type: 'object',
131+
},
132+
},
133+
},
134+
paths: {
135+
'/api/items': {
136+
get: {
137+
responses: {
138+
'200': {
139+
content: {
140+
'application/json': {
141+
// URL-encoded $ref simulating what json-schema-ref-parser produces
142+
schema: { $ref: '#/components/schemas/PaginatedList%3CClientItem%3E' },
143+
},
144+
},
145+
description: '',
146+
},
147+
},
148+
tags: ['Client'],
149+
},
150+
},
151+
'/api/other': {
152+
get: {
153+
responses: {
154+
'200': {
155+
content: {
156+
'application/json': {
157+
schema: { $ref: '#/components/schemas/OtherResponse' },
158+
},
159+
},
160+
description: '',
161+
},
162+
},
163+
tags: ['Other'],
164+
},
165+
},
166+
},
167+
};
168+
169+
const { graph } = buildGraph(spec, loggerStub);
170+
const { resourceMetadata } = buildResourceMetadata(graph, loggerStub);
171+
172+
const filters = createFilters();
173+
filters.tags.include.add('Client');
174+
175+
const { operations } = createFilteredDependencies({
176+
filters,
177+
logger: loggerStub,
178+
resourceMetadata,
179+
});
180+
181+
// The 'Client' tagged operation must be included despite the URL-encoded $ref
182+
expect(operations.has('operation/GET /api/items')).toBe(true);
183+
// The 'Other' tagged operation must be excluded
184+
expect(operations.has('operation/GET /api/other')).toBe(false);
185+
});
186+
});

packages/shared/src/openApi/shared/graph/meta.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ export const buildResourceMetadata = (
7777
if (namespace === 'unknown') {
7878
console.warn(`unsupported type: ${type}`);
7979
}
80-
dependencies.add(addNamespace(namespace, name));
80+
// Decode URI components to handle cases where $ref values were
81+
// URL-encoded by the JSON schema ref-parser (e.g. angle brackets
82+
// in generic schema names like Foo<Bar> become Foo%3CBar%3E).
83+
dependencies.add(addNamespace(namespace, decodeURI(name)));
8184
}
8285
}
8386
}

0 commit comments

Comments
 (0)