Skip to content

Commit 19be9ea

Browse files
kennyderekdevin-ai-integration[bot]kenny@buildwithfern.com
authored
fix(cli): resolve external references, error throwing, parse external security (#9929)
* resolve external references, error throwing, parse external security schemas * fix * fixes * fixes * chore: convert logger.error and logger.warn to logger.debug Co-Authored-By: [email protected] <[email protected]> * chore: fix lint issues in test file Co-Authored-By: [email protected] <[email protected]> * chore: fix remaining lint issues in test file Co-Authored-By: [email protected] <[email protected]> * chore: add biome-ignore comments for test file lint issues Co-Authored-By: [email protected] <[email protected]> * Remove superfluous comment marker Co-Authored-By: [email protected] <[email protected]> * feat: add test and fix for nested external references with internal dependencies - Add test case for schema A referencing schema B in doc1, where B uses both internal refs and external refs to doc2 - Fix DocumentPreprocessor to import internal components from external documents when resolving specific components - This ensures that when importing SchemaB from doc1, we also import InternalComponent that SchemaB references - Fixes error: 'Schema X does not exist' when external schemas reference internal components Co-Authored-By: [email protected] <[email protected]> * fix: resolve CI/CD errors - fix compile and lint issues Co-Authored-By: [email protected] <[email protected]> * fix: remove unused import and fix formatting Co-Authored-By: [email protected] <[email protected]> * update test snaps * fixes * minimal changes * rm asyncs * lockfile * test update * test update * test snaps * versions.yml --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent 9ab6487 commit 19be9ea

File tree

16 files changed

+4473
-7623
lines changed

16 files changed

+4473
-7623
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { Source } from "@fern-api/openapi-ir";
2+
import { TaskContext } from "@fern-api/task-context";
3+
import { OpenAPIV3 } from "openapi-types";
4+
import { describe, expect, it, vi } from "vitest";
5+
import { convertSecurityScheme } from "../openapi/v3/converters/convertSecurityScheme";
6+
import { OpenAPIV3ParserContext } from "../openapi/v3/OpenAPIV3ParserContext";
7+
import { DEFAULT_PARSE_OPENAPI_SETTINGS } from "../options";
8+
9+
describe("convertSecurityScheme", () => {
10+
const mockTaskContext = {
11+
logger: {
12+
debug: vi.fn(),
13+
info: vi.fn(),
14+
warn: vi.fn(),
15+
error: vi.fn()
16+
}
17+
} as unknown as TaskContext;
18+
19+
const source: Source = Source.openapi({
20+
file: "test.yaml"
21+
});
22+
23+
it("should convert referenced security schemes", () => {
24+
// Create an OpenAPI document with a referenced security scheme
25+
const openApiDocument: OpenAPIV3.Document = {
26+
openapi: "3.0.0",
27+
info: {
28+
title: "Test API",
29+
version: "1.0.0"
30+
},
31+
paths: {},
32+
components: {
33+
securitySchemes: {
34+
BearerAuth: {
35+
type: "http",
36+
scheme: "bearer",
37+
bearerFormat: "JWT"
38+
}
39+
}
40+
}
41+
};
42+
43+
// Create a context for reference resolution
44+
const context = new OpenAPIV3ParserContext({
45+
document: openApiDocument,
46+
taskContext: mockTaskContext,
47+
authHeaders: new Set(),
48+
options: DEFAULT_PARSE_OPENAPI_SETTINGS,
49+
source,
50+
namespace: undefined
51+
});
52+
53+
// Create a reference to the security scheme
54+
const securitySchemeRef: OpenAPIV3.ReferenceObject = {
55+
$ref: "#/components/securitySchemes/BearerAuth"
56+
};
57+
58+
// Convert the referenced security scheme
59+
const result = convertSecurityScheme(securitySchemeRef, source, mockTaskContext, context);
60+
61+
// Verify the result
62+
expect(result).toBeDefined();
63+
expect(result?.type).toBe("bearer");
64+
if (result?.type === "bearer") {
65+
expect(result.tokenVariableName).toBeUndefined();
66+
expect(result.tokenEnvVar).toBeUndefined();
67+
}
68+
});
69+
70+
it("should throw an error when resolving reference without context", () => {
71+
const securitySchemeRef: OpenAPIV3.ReferenceObject = {
72+
$ref: "#/components/securitySchemes/BearerAuth"
73+
};
74+
75+
expect(() => {
76+
convertSecurityScheme(securitySchemeRef, source, mockTaskContext);
77+
}).toThrow("Converting referenced security schemes requires context");
78+
});
79+
80+
it("should convert direct security schemes without context", () => {
81+
const securityScheme: OpenAPIV3.SecuritySchemeObject = {
82+
type: "http",
83+
scheme: "bearer",
84+
bearerFormat: "JWT"
85+
};
86+
87+
const result = convertSecurityScheme(securityScheme, source, mockTaskContext);
88+
89+
expect(result).toBeDefined();
90+
expect(result?.type).toBe("bearer");
91+
if (result?.type === "bearer") {
92+
expect(result.tokenVariableName).toBeUndefined();
93+
expect(result.tokenEnvVar).toBeUndefined();
94+
}
95+
});
96+
});

packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/AbstractOpenAPIV3ParserContext.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const PARAMETER_REFERENCE_PREFIX = "#/components/parameters/";
1313
export const RESPONSE_REFERENCE_PREFIX = "#/components/responses/";
1414
export const EXAMPLES_REFERENCE_PREFIX = "#/components/examples/";
1515
export const REQUEST_BODY_REFERENCE_PREFIX = "#/components/requestBodies/";
16+
export const SECURITY_SCHEME_REFERENCE_PREFIX = "#/components/securitySchemes/";
1617

1718
export interface DiscriminatedUnionReference {
1819
discriminants: Set<string>;
@@ -221,6 +222,25 @@ export abstract class AbstractOpenAPIV3ParserContext implements SchemaParserCont
221222
return resolvedExample;
222223
}
223224

225+
public resolveSecuritySchemeReference(securityScheme: OpenAPIV3.ReferenceObject): OpenAPIV3.SecuritySchemeObject {
226+
if (
227+
this.document.components == null ||
228+
this.document.components.securitySchemes == null ||
229+
!securityScheme.$ref.startsWith(SECURITY_SCHEME_REFERENCE_PREFIX)
230+
) {
231+
throw new Error(`Failed to resolve ${securityScheme.$ref}`);
232+
}
233+
const securitySchemeKey = securityScheme.$ref.substring(SECURITY_SCHEME_REFERENCE_PREFIX.length);
234+
const resolvedSecurityScheme = this.document.components.securitySchemes[securitySchemeKey];
235+
if (resolvedSecurityScheme == null) {
236+
throw new Error(`${securityScheme.$ref} is undefined`);
237+
}
238+
if (isReferenceObject(resolvedSecurityScheme)) {
239+
return this.resolveSecuritySchemeReference(resolvedSecurityScheme);
240+
}
241+
return resolvedSecurityScheme;
242+
}
243+
224244
public referenceExists(ref: string): boolean {
225245
// Step 1: Get keys
226246
const keys = ref

packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/converters/convertSecurityScheme.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getExtension } from "../../../getExtension";
55
import { convertEnum } from "../../../schema/convertEnum";
66
import { convertSchemaWithExampleToSchema } from "../../../schema/utils/convertSchemaWithExampleToSchema";
77
import { isReferenceObject } from "../../../schema/utils/isReferenceObject";
8+
import { AbstractOpenAPIV3ParserContext } from "../AbstractOpenAPIV3ParserContext";
89
import { OpenAPIExtension } from "../extensions/extensions";
910
import { FernOpenAPIExtension } from "../extensions/fernExtensions";
1011
import { getBasicSecuritySchemeNames } from "../extensions/getBasicSecuritySchemeNames";
@@ -17,10 +18,17 @@ import {
1718
export function convertSecurityScheme(
1819
securityScheme: OpenAPIV3.SecuritySchemeObject | OpenAPIV3.ReferenceObject,
1920
source: Source,
20-
taskContext: TaskContext
21+
taskContext: TaskContext,
22+
context?: AbstractOpenAPIV3ParserContext
2123
): SecurityScheme | undefined {
2224
if (isReferenceObject(securityScheme)) {
23-
throw new Error(`Converting referenced security schemes is unsupported: ${JSON.stringify(securityScheme)}`);
25+
if (context == null) {
26+
throw new Error(
27+
`Converting referenced security schemes requires context: ${JSON.stringify(securityScheme)}`
28+
);
29+
}
30+
const resolvedSecurityScheme = context.resolveSecuritySchemeReference(securityScheme);
31+
return convertSecuritySchemeHelper(resolvedSecurityScheme, source, taskContext);
2432
}
2533
return convertSecuritySchemeHelper(securityScheme, source, taskContext);
2634
}

packages/cli/api-importers/openapi/openapi-ir-parser/src/openapi/v3/generateIr.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,20 @@ export function generateIr({
6565
}): OpenApiIntermediateRepresentation {
6666
openApi = runResolutions({ openapi: openApi });
6767

68+
// Create a temporary context for reference resolution during security scheme processing
69+
const tempContext = new OpenAPIV3ParserContext({
70+
document: openApi,
71+
taskContext,
72+
authHeaders: new Set(), // temporary empty set
73+
options,
74+
source,
75+
namespace
76+
});
77+
6878
const securitySchemes: Record<string, SecurityScheme> = Object.fromEntries(
6979
Object.entries(openApi.components?.securitySchemes ?? {})
7080
.map(([key, securityScheme]) => {
71-
const convertedSecurityScheme = convertSecurityScheme(securityScheme, source, taskContext);
81+
const convertedSecurityScheme = convertSecurityScheme(securityScheme, source, taskContext, tempContext);
7282
if (convertedSecurityScheme == null) {
7383
return null;
7484
}

packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-docs/url-reference.json

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,27 @@
1717
{
1818
"response": {
1919
"body": {
20-
"key": "value",
20+
"category": {
21+
"id": 6,
22+
"name": "name",
23+
},
24+
"id": 0,
25+
"name": "doggie",
26+
"photoUrls": [
27+
"photoUrls",
28+
"photoUrls",
29+
],
30+
"status": "available",
31+
"tags": [
32+
{
33+
"id": 1,
34+
"name": "name",
35+
},
36+
{
37+
"id": 1,
38+
"name": "name",
39+
},
40+
],
2141
},
2242
},
2343
},
@@ -28,7 +48,7 @@
2848
"response": {
2949
"docs": "A list of pets",
3050
"status-code": 200,
31-
"type": "unknown",
51+
"type": "Pet",
3252
},
3353
"source": {
3454
"openapi": "../openapi.yml",
@@ -40,6 +60,17 @@
4060
},
4161
},
4262
"types": {
63+
"Category": {
64+
"docs": "A category for a pet",
65+
"inline": undefined,
66+
"properties": {
67+
"id": "optional<long>",
68+
"name": "optional<string>",
69+
},
70+
"source": {
71+
"openapi": "../openapi.yml",
72+
},
73+
},
4374
"Error": {
4475
"docs": undefined,
4576
"inline": undefined,
@@ -51,6 +82,47 @@
5182
"openapi": "../openapi.yml",
5283
},
5384
},
85+
"Pet": {
86+
"docs": "A pet for sale in the pet store",
87+
"inline": undefined,
88+
"properties": {
89+
"category": "optional<Category>",
90+
"id": "optional<long>",
91+
"name": "string",
92+
"photoUrls": "list<string>",
93+
"status": {
94+
"docs": "pet status in the store",
95+
"type": "optional<PetStatus>",
96+
},
97+
"tags": "optional<list<Tag>>",
98+
},
99+
"source": {
100+
"openapi": "../openapi.yml",
101+
},
102+
},
103+
"PetStatus": {
104+
"docs": "pet status in the store",
105+
"enum": [
106+
"available",
107+
"pending",
108+
"sold",
109+
],
110+
"inline": true,
111+
"source": {
112+
"openapi": "../openapi.yml",
113+
},
114+
},
115+
"Tag": {
116+
"docs": "A tag for a pet",
117+
"inline": undefined,
118+
"properties": {
119+
"id": "optional<long>",
120+
"name": "optional<string>",
121+
},
122+
"source": {
123+
"openapi": "../openapi.yml",
124+
},
125+
},
54126
},
55127
},
56128
"rawContents": "service:
@@ -65,12 +137,25 @@
65137
display-name: List all pets
66138
response:
67139
docs: A list of pets
68-
type: unknown
140+
type: Pet
69141
status-code: 200
70142
examples:
71143
- response:
72144
body:
73-
key: value
145+
id: 0
146+
category:
147+
id: 6
148+
name: name
149+
name: doggie
150+
photoUrls:
151+
- photoUrls
152+
- photoUrls
153+
tags:
154+
- id: 1
155+
name: name
156+
- id: 1
157+
name: name
158+
status: available
74159
source:
75160
openapi: ../openapi.yml
76161
types:
@@ -80,6 +165,42 @@ types:
80165
message: optional<string>
81166
source:
82167
openapi: ../openapi.yml
168+
Category:
169+
docs: A category for a pet
170+
properties:
171+
id: optional<long>
172+
name: optional<string>
173+
source:
174+
openapi: ../openapi.yml
175+
Tag:
176+
docs: A tag for a pet
177+
properties:
178+
id: optional<long>
179+
name: optional<string>
180+
source:
181+
openapi: ../openapi.yml
182+
PetStatus:
183+
enum:
184+
- available
185+
- pending
186+
- sold
187+
docs: pet status in the store
188+
inline: true
189+
source:
190+
openapi: ../openapi.yml
191+
Pet:
192+
docs: A pet for sale in the pet store
193+
properties:
194+
id: optional<long>
195+
category: optional<Category>
196+
name: string
197+
photoUrls: list<string>
198+
tags: optional<list<Tag>>
199+
status:
200+
type: optional<PetStatus>
201+
docs: pet status in the store
202+
source:
203+
openapi: ../openapi.yml
83204
",
84205
},
85206
},

0 commit comments

Comments
 (0)