Skip to content

Commit 6793bbd

Browse files
authored
fix: openapi securitySchemas with referenced schemas (#647)
1 parent 6349265 commit 6793bbd

File tree

3 files changed

+129
-101
lines changed

3 files changed

+129
-101
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect-http": patch
3+
---
4+
5+
Fix #646 - securitySchemes was overwritten by reference schemas.

packages/effect-http/src/internal/open-api.ts

Lines changed: 103 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -38,143 +38,141 @@ export const make = (
3838
}
3939

4040
api.groups.forEach((group) =>
41-
group.endpoints.forEach(
42-
(endpoint) => {
43-
const options = ApiEndpoint.getOptions(endpoint)
44-
const security = ApiEndpoint.getSecurity(endpoint)
45-
46-
const operationSpec: OpenApiTypes.OpenAPISpecOperation = {
47-
operationId: ApiEndpoint.getId(endpoint),
48-
tags: [group.name]
49-
}
41+
group.endpoints.forEach((endpoint) => {
42+
const options = ApiEndpoint.getOptions(endpoint)
43+
const security = ApiEndpoint.getSecurity(endpoint)
5044

51-
for (const response of ApiEndpoint.getResponse(endpoint)) {
52-
const body = ApiResponse.getBodySchema(response)
53-
const headers = ApiResponse.getHeadersSchema(response)
54-
const status = ApiResponse.getStatus(response)
45+
const operationSpec: OpenApiTypes.OpenAPISpecOperation = {
46+
operationId: ApiEndpoint.getId(endpoint),
47+
tags: [group.name]
48+
}
5549

56-
const responseSpec: OpenApiTypes.OpenApiSpecResponse = { description: `Response ${status}` }
50+
for (const response of ApiEndpoint.getResponse(endpoint)) {
51+
const body = ApiResponse.getBodySchema(response)
52+
const headers = ApiResponse.getHeadersSchema(response)
53+
const status = ApiResponse.getStatus(response)
5754

58-
if (!ApiSchema.isIgnored(body) && !ApiSchema.isIgnored(headers)) {
59-
responseSpec.description = "No content"
60-
}
55+
const responseSpec: OpenApiTypes.OpenApiSpecResponse = { description: `Response ${status}` }
6156

62-
if (!ApiSchema.isIgnored(body)) {
63-
const content: OpenApiTypes.OpenApiSpecMediaType = {
64-
schema: makeSchema(body, addComponentSchemaCallback)
65-
}
57+
if (!ApiSchema.isIgnored(body) && !ApiSchema.isIgnored(headers)) {
58+
responseSpec.description = "No content"
59+
}
6660

67-
const description = AST.getDescriptionAnnotation(body.ast)
61+
if (!ApiSchema.isIgnored(body)) {
62+
const content: OpenApiTypes.OpenApiSpecMediaType = {
63+
schema: makeSchema(body, addComponentSchemaCallback)
64+
}
6865

69-
if (Option.isSome(description)) {
70-
content.description = description.value
71-
}
66+
const description = AST.getDescriptionAnnotation(body.ast)
7267

73-
responseSpec.content = {
74-
"application/json": content
75-
}
68+
if (Option.isSome(description)) {
69+
content.description = description.value
7670
}
7771

78-
if (!ApiSchema.isIgnored(headers)) {
79-
responseSpec.headers = createResponseHeaders(headers, addComponentSchemaCallback)
72+
responseSpec.content = {
73+
"application/json": content
8074
}
75+
}
8176

82-
operationSpec.responses = {
83-
...operationSpec.responses,
84-
[String(status)]: responseSpec
85-
}
77+
if (!ApiSchema.isIgnored(headers)) {
78+
responseSpec.headers = createResponseHeaders(headers, addComponentSchemaCallback)
8679
}
8780

88-
const addParameters = (
89-
type: "query" | "header" | "path",
90-
schema: Schema.Schema.Any
91-
) => {
92-
let parameters: Array<OpenApiTypes.OpenAPISpecParameter> = []
81+
operationSpec.responses = {
82+
...operationSpec.responses,
83+
[String(status)]: responseSpec
84+
}
85+
}
9386

94-
if (operationSpec.parameters === undefined) {
95-
operationSpec.parameters = parameters
96-
} else {
97-
parameters = operationSpec.parameters
98-
}
87+
const addParameters = (
88+
type: "query" | "header" | "path",
89+
schema: Schema.Schema.Any
90+
) => {
91+
let parameters: Array<OpenApiTypes.OpenAPISpecParameter> = []
9992

100-
parameters.push(...createParameters(type, schema, addComponentSchemaCallback))
93+
if (operationSpec.parameters === undefined) {
94+
operationSpec.parameters = parameters
95+
} else {
96+
parameters = operationSpec.parameters
10197
}
10298

103-
const request = ApiEndpoint.getRequest(endpoint)
104-
105-
const body = ApiRequest.getBodySchema(request)
106-
const headers = ApiRequest.getHeadersSchema(request)
107-
const path = ApiRequest.getPathSchema(request)
108-
const query = ApiRequest.getQuerySchema(request)
99+
parameters.push(...createParameters(type, schema, addComponentSchemaCallback))
100+
}
109101

110-
if (!ApiSchema.isIgnored(path)) {
111-
addParameters("path", path)
112-
}
102+
const request = ApiEndpoint.getRequest(endpoint)
113103

114-
if (!ApiSchema.isIgnored(query)) {
115-
addParameters("query", query)
116-
}
104+
const body = ApiRequest.getBodySchema(request)
105+
const headers = ApiRequest.getHeadersSchema(request)
106+
const path = ApiRequest.getPathSchema(request)
107+
const query = ApiRequest.getQuerySchema(request)
117108

118-
if (!ApiSchema.isIgnored(headers)) {
119-
addParameters("header", headers)
120-
}
109+
if (!ApiSchema.isIgnored(path)) {
110+
addParameters("path", path)
111+
}
121112

122-
if (!ApiSchema.isIgnored(body)) {
123-
operationSpec.requestBody = {
124-
content: {
125-
"application/json": {
126-
schema: makeSchema(body, addComponentSchemaCallback)
127-
}
128-
},
129-
required: true
130-
}
113+
if (!ApiSchema.isIgnored(query)) {
114+
addParameters("query", query)
115+
}
131116

132-
const description = AST.getDescriptionAnnotation(body.ast)
117+
if (!ApiSchema.isIgnored(headers)) {
118+
addParameters("header", headers)
119+
}
133120

134-
if (Option.isSome(description)) {
135-
operationSpec.requestBody.description = description.value
136-
}
121+
if (!ApiSchema.isIgnored(body)) {
122+
operationSpec.requestBody = {
123+
content: {
124+
"application/json": {
125+
schema: makeSchema(body, addComponentSchemaCallback)
126+
}
127+
},
128+
required: true
137129
}
138130

139-
const securityList = Record.toEntries(Security.getOpenApi(security))
131+
const description = AST.getDescriptionAnnotation(body.ast)
140132

141-
if (securityList.length > 0) {
142-
operationSpec.security = securityList.map(([name]) => ({ [name]: [] }))
133+
if (Option.isSome(description)) {
134+
operationSpec.requestBody.description = description.value
143135
}
136+
}
144137

145-
for (const [name, schemes] of securityList) {
146-
const securitySchemes = openApi.components?.securitySchemes ?? {}
147-
securitySchemes[name] = schemes as OpenApiTypes.OpenAPISecurityScheme
138+
const securityList = Record.toEntries(Security.getOpenApi(security))
148139

149-
const components = openApi.components ?? {}
150-
components.securitySchemes = securitySchemes
140+
if (securityList.length > 0) {
141+
operationSpec.security = securityList.map(([name]) => ({ [name]: [] }))
142+
}
151143

152-
openApi.components = components
153-
}
144+
for (const [name, schemes] of securityList) {
145+
const securitySchemes = openApi.components?.securitySchemes ?? {}
146+
securitySchemes[name] = schemes as OpenApiTypes.OpenAPISecurityScheme
154147

155-
if (options.description !== undefined) {
156-
operationSpec.description = options.description
157-
}
148+
const components = openApi.components ?? {}
149+
components.securitySchemes = securitySchemes
158150

159-
if (options.summary !== undefined) {
160-
operationSpec.summary = options.summary
161-
}
151+
openApi.components = components
152+
}
162153

163-
if (options.deprecated) {
164-
operationSpec["deprecated"] = true
165-
}
154+
if (options.description !== undefined) {
155+
operationSpec.description = options.description
156+
}
166157

167-
const pathName = createPath(ApiEndpoint.getPath(endpoint))
158+
if (options.summary !== undefined) {
159+
operationSpec.summary = options.summary
160+
}
168161

169-
openApi.paths = {
170-
...openApi.paths,
171-
[pathName]: {
172-
...openApi.paths[pathName],
173-
[ApiEndpoint.getMethod(endpoint).toLowerCase() as OpenApiTypes.OpenAPISpecMethodName]: operationSpec
174-
}
162+
if (options.deprecated) {
163+
operationSpec["deprecated"] = true
164+
}
165+
166+
const pathName = createPath(ApiEndpoint.getPath(endpoint))
167+
168+
openApi.paths = {
169+
...openApi.paths,
170+
[pathName]: {
171+
...openApi.paths[pathName],
172+
[ApiEndpoint.getMethod(endpoint).toLowerCase() as OpenApiTypes.OpenAPISpecMethodName]: operationSpec
175173
}
176174
}
177-
)
175+
})
178176
)
179177

180178
if (Array.isNonEmptyReadonlyArray(api.groups)) {
@@ -206,7 +204,11 @@ export const make = (
206204
schemas[name] = openAPISchemaForAst(removeIdentifierAnnotation(ast), addComponentSchemaCallback)
207205
}
208206

209-
openApi.components = { schemas }
207+
if (openApi.components === undefined) {
208+
openApi.components = { schemas }
209+
}
210+
211+
openApi.components.schemas = schemas
210212
}
211213

212214
return openApi

packages/effect-http/test/openapi.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,4 +1489,25 @@ describe("component schema and reference", () => {
14891489
// @ts-expect-error
14901490
SwaggerParser.validate(spec)
14911491
})
1492+
1493+
it("reference and security", async () => {
1494+
class ReferencedType extends Schema.Class<ReferencedType>("ReferencedType")(
1495+
Schema.Struct({ something: Schema.String })
1496+
) {}
1497+
1498+
const api = Api.make({ title: "test", version: "0.1" }).pipe(
1499+
Api.addEndpoint(
1500+
Api.post("getPet", "/pet").pipe(
1501+
Api.setResponseBody(ReferencedType),
1502+
Api.setSecurity(Security.basic())
1503+
)
1504+
)
1505+
)
1506+
const spec = OpenApi.make(api)
1507+
expect(spec.components?.schemas).toBeTruthy()
1508+
expect(spec.components?.securitySchemes).toBeTruthy()
1509+
1510+
// @ts-expect-error
1511+
SwaggerParser.validate(spec)
1512+
})
14921513
})

0 commit comments

Comments
 (0)