Skip to content

Commit 309a8a4

Browse files
author
Andy Nguyen
committed
fix(openapi): avoid OAuth scope type collisions
1 parent b67ee8d commit 309a8a4

7 files changed

Lines changed: 342 additions & 1 deletion

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"specVersion": "1.0.0",
3+
"title": "OAuth scope name collision",
4+
"servers": [],
5+
"websocketServers": [],
6+
"tags": {
7+
"tagsById": {}
8+
},
9+
"hasEndpointsMarkedInternal": false,
10+
"endpoints": [
11+
{
12+
"audiences": [],
13+
"operationId": "getScope",
14+
"tags": [],
15+
"pathParameters": [],
16+
"queryParameters": [],
17+
"headers": [],
18+
"generatedRequestName": "GetScopeRequest",
19+
"response": {
20+
"description": "Success",
21+
"schema": {
22+
"generatedName": "GetScopeResponse",
23+
"schema": "OAuthScope",
24+
"source": {
25+
"file": "../openapi.yml",
26+
"type": "openapi"
27+
},
28+
"type": "reference"
29+
},
30+
"fullExamples": [],
31+
"source": {
32+
"file": "../openapi.yml",
33+
"type": "openapi"
34+
},
35+
"statusCode": 200,
36+
"type": "json"
37+
},
38+
"errors": {},
39+
"servers": [],
40+
"authed": true,
41+
"security": [
42+
{
43+
"OAuth2": [
44+
"read"
45+
]
46+
}
47+
],
48+
"method": "GET",
49+
"path": "/scope",
50+
"examples": [
51+
{
52+
"pathParameters": [],
53+
"queryParameters": [],
54+
"headers": [],
55+
"response": {
56+
"value": {
57+
"value": {
58+
"value": "string",
59+
"type": "string"
60+
},
61+
"type": "primitive"
62+
},
63+
"type": "withoutStreaming"
64+
},
65+
"codeSamples": [],
66+
"type": "full"
67+
}
68+
],
69+
"source": {
70+
"file": "../openapi.yml",
71+
"type": "openapi"
72+
}
73+
}
74+
],
75+
"webhooks": [],
76+
"channels": {},
77+
"groupedSchemas": {
78+
"rootSchemas": {
79+
"OAuthScope": {
80+
"schema": {
81+
"type": "string"
82+
},
83+
"generatedName": "OAuthScope",
84+
"groupName": [],
85+
"type": "primitive"
86+
},
87+
"OauthAuthorizationScope": {
88+
"schema": {
89+
"type": "string"
90+
},
91+
"generatedName": "OauthAuthorizationScope",
92+
"groupName": [
93+
"grouped"
94+
],
95+
"type": "primitive"
96+
}
97+
},
98+
"namespacedSchemas": {}
99+
},
100+
"variables": {},
101+
"nonRequestReferencedSchemas": {},
102+
"securitySchemes": {
103+
"OAuth2": {
104+
"scopesEnum": {
105+
"generatedName": "OauthScope",
106+
"values": [
107+
{
108+
"generatedName": "read",
109+
"value": "read",
110+
"description": "Read access",
111+
"casing": {}
112+
}
113+
],
114+
"source": {
115+
"file": "../openapi.yml",
116+
"type": "openapi"
117+
},
118+
"type": "enum"
119+
},
120+
"type": "oauth"
121+
}
122+
},
123+
"globalHeaders": [],
124+
"idempotencyHeaders": [],
125+
"groups": {}
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
{
2+
"absoluteFilePath": "/DUMMY_PATH",
3+
"importedDefinitions": {},
4+
"namedDefinitionFiles": {
5+
"__package__.yml": {
6+
"absoluteFilepath": "/DUMMY_PATH",
7+
"contents": {
8+
"service": {
9+
"auth": false,
10+
"base-path": "",
11+
"endpoints": {
12+
"getScope": {
13+
"auth": [
14+
{
15+
"OAuth2": [
16+
"read",
17+
],
18+
},
19+
],
20+
"docs": undefined,
21+
"examples": [
22+
{
23+
"response": {
24+
"body": "string",
25+
},
26+
},
27+
],
28+
"method": "GET",
29+
"pagination": undefined,
30+
"path": "/scope",
31+
"response": {
32+
"docs": "Success",
33+
"status-code": 200,
34+
"type": "OAuthScope",
35+
},
36+
"source": {
37+
"openapi": "../openapi.yml",
38+
},
39+
},
40+
},
41+
"source": {
42+
"openapi": "../openapi.yml",
43+
},
44+
},
45+
"types": {
46+
"OAuthScope": "string",
47+
"OauthAuthorizationScope": {
48+
"enum": [
49+
{
50+
"docs": "Read access",
51+
"value": "read",
52+
},
53+
],
54+
"inline": undefined,
55+
"source": {
56+
"openapi": "../openapi.yml",
57+
},
58+
},
59+
},
60+
},
61+
"rawContents": "types:
62+
OauthAuthorizationScope:
63+
enum:
64+
- value: read
65+
docs: Read access
66+
source:
67+
openapi: ../openapi.yml
68+
OAuthScope: string
69+
service:
70+
auth: false
71+
base-path: ''
72+
endpoints:
73+
getScope:
74+
path: /scope
75+
method: GET
76+
auth:
77+
- OAuth2:
78+
- read
79+
source:
80+
openapi: ../openapi.yml
81+
response:
82+
docs: Success
83+
type: OAuthScope
84+
status-code: 200
85+
examples:
86+
- response:
87+
body: string
88+
source:
89+
openapi: ../openapi.yml
90+
",
91+
},
92+
"grouped.yml": {
93+
"absoluteFilepath": "/DUMMY_PATH",
94+
"contents": {
95+
"types": {
96+
"OauthAuthorizationScope": "string",
97+
},
98+
},
99+
"rawContents": "types:
100+
OauthAuthorizationScope: string
101+
",
102+
},
103+
},
104+
"packageMarkers": {},
105+
"rootApiFile": {
106+
"contents": {
107+
"auth": "OAuth2",
108+
"auth-schemes": {
109+
"OAuth2": {
110+
"scheme": "bearer",
111+
},
112+
},
113+
"display-name": "OAuth scope name collision",
114+
"error-discrimination": {
115+
"strategy": "status-code",
116+
},
117+
"name": "api",
118+
},
119+
"defaultUrl": undefined,
120+
"rawContents": "name: api
121+
error-discrimination:
122+
strategy: status-code
123+
display-name: OAuth scope name collision
124+
auth-schemes:
125+
OAuth2:
126+
scheme: bearer
127+
auth: OAuth2
128+
",
129+
},
130+
"specVersion": "1.0.0",
131+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"organization": "fern",
3+
"version": "*"
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json
2+
api:
3+
specs:
4+
- openapi: ../openapi.yml
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
openapi: 3.0.3
2+
info:
3+
title: OAuth scope name collision
4+
version: 1.0.0
5+
6+
paths:
7+
/scope:
8+
get:
9+
operationId: getScope
10+
security:
11+
- OAuth2:
12+
- read
13+
responses:
14+
"200":
15+
description: Success
16+
content:
17+
application/json:
18+
schema:
19+
$ref: "#/components/schemas/OAuthScope"
20+
21+
components:
22+
schemas:
23+
OAuthScope:
24+
type: string
25+
OauthAuthorizationScope:
26+
type: string
27+
x-fern-sdk-group-name: grouped
28+
29+
securitySchemes:
30+
OAuth2:
31+
type: oauth2
32+
flows:
33+
clientCredentials:
34+
tokenUrl: https://example.com/oauth/token
35+
scopes:
36+
read: Read access

packages/cli/api-importers/openapi/openapi-ir-to-fern/src/buildAuthSchemes.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import { FERN_PACKAGE_MARKER_FILENAME } from "@fern-api/configuration";
12
import { RawSchemas } from "@fern-api/fern-definition-schema";
3+
import type { Schema } from "@fern-api/openapi-ir";
24
import { RelativeFilePath } from "@fern-api/path-utils";
35
import { buildEnumTypeDeclaration } from "./buildTypeDeclaration.js";
46
import { OpenApiIrConverterContext } from "./OpenApiIrConverterContext.js";
7+
import { getDeclarationFileForSchema } from "./utils/getDeclarationFileForSchema.js";
58
import { getHeaderName } from "./utils/getHeaderName.js";
69

710
const BASIC_AUTH_SCHEME = "BasicAuthScheme";
811
const BEARER_AUTH_SCHEME = "BearerAuthScheme";
12+
const OAUTH_SCOPE_TYPE_NAME = "OauthScope";
13+
const OAUTH_SCOPE_FALLBACK_TYPE_NAME = "OauthAuthorizationScope";
914

1015
export function buildAuthSchemes(context: OpenApiIrConverterContext): void {
1116
if (context.authOverrides != null) {
@@ -22,6 +27,7 @@ export function buildAuthSchemes(context: OpenApiIrConverterContext): void {
2227
}
2328

2429
let setAuth = false;
30+
let oauthScopeTypeName: string | undefined;
2531

2632
for (const [id, securityScheme] of Object.entries(context.ir.securitySchemes)) {
2733
if (securityScheme.type === "basic") {
@@ -179,11 +185,40 @@ export function buildAuthSchemes(context: OpenApiIrConverterContext): void {
179185
setAuth = true;
180186
}
181187
if (securityScheme.scopesEnum != null && securityScheme.scopesEnum.values.length > 0) {
188+
oauthScopeTypeName ??= getOauthScopeTypeName(context);
182189
context.builder.addType(RelativeFilePath.of("__package__.yml"), {
183-
name: "OauthScope",
190+
name: oauthScopeTypeName,
184191
schema: buildEnumTypeDeclaration(securityScheme.scopesEnum, 0).schema
185192
});
186193
}
187194
}
188195
}
189196
}
197+
198+
function getOauthScopeTypeName(context: OpenApiIrConverterContext): string {
199+
const occupiedTypeNames = new Set(
200+
Object.values(context.ir.groupedSchemas.rootSchemas)
201+
.filter(
202+
(schema) =>
203+
getDeclarationFileForSchema(schema) === RelativeFilePath.of(FERN_PACKAGE_MARKER_FILENAME)
204+
)
205+
.map((schema) => getSchemaName(schema).toLowerCase())
206+
);
207+
if (!occupiedTypeNames.has(OAUTH_SCOPE_TYPE_NAME.toLowerCase())) {
208+
return OAUTH_SCOPE_TYPE_NAME;
209+
}
210+
if (!occupiedTypeNames.has(OAUTH_SCOPE_FALLBACK_TYPE_NAME.toLowerCase())) {
211+
return OAUTH_SCOPE_FALLBACK_TYPE_NAME;
212+
}
213+
214+
let suffix = 2;
215+
while (occupiedTypeNames.has(`${OAUTH_SCOPE_FALLBACK_TYPE_NAME}${suffix}`.toLowerCase())) {
216+
suffix++;
217+
}
218+
return `${OAUTH_SCOPE_FALLBACK_TYPE_NAME}${suffix}`;
219+
}
220+
221+
function getSchemaName(schema: Schema): string {
222+
const namedSchema = schema.type === "oneOf" ? schema.value : schema;
223+
return namedSchema.nameOverride ?? namedSchema.generatedName;
224+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json
2+
3+
- summary: |
4+
Avoid collisions between OpenAPI component names and the generated OAuth scope enum.
5+
type: fix

0 commit comments

Comments
 (0)