@effect/openapi-generator 4.0.0-beta.8 → 4.0.0-beta.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/HttpApiTransformer.d.ts +41 -0
  2. package/dist/HttpApiTransformer.d.ts.map +1 -0
  3. package/dist/HttpApiTransformer.js +383 -0
  4. package/dist/HttpApiTransformer.js.map +1 -0
  5. package/dist/JsonSchemaGenerator.d.ts +25 -3
  6. package/dist/JsonSchemaGenerator.d.ts.map +1 -1
  7. package/dist/JsonSchemaGenerator.js +178 -47
  8. package/dist/JsonSchemaGenerator.js.map +1 -1
  9. package/dist/OpenApiGenerator.d.ts +81 -7
  10. package/dist/OpenApiGenerator.d.ts.map +1 -1
  11. package/dist/OpenApiGenerator.js +707 -119
  12. package/dist/OpenApiGenerator.js.map +1 -1
  13. package/dist/OpenApiPatch.d.ts +47 -24
  14. package/dist/OpenApiPatch.d.ts.map +1 -1
  15. package/dist/OpenApiPatch.js +42 -19
  16. package/dist/OpenApiPatch.js.map +1 -1
  17. package/dist/OpenApiTransformer.d.ts +85 -12
  18. package/dist/OpenApiTransformer.d.ts.map +1 -1
  19. package/dist/OpenApiTransformer.js +85 -8
  20. package/dist/OpenApiTransformer.js.map +1 -1
  21. package/dist/ParsedOperation.d.ts +172 -1
  22. package/dist/ParsedOperation.d.ts.map +1 -1
  23. package/dist/ParsedOperation.js +32 -0
  24. package/dist/ParsedOperation.js.map +1 -1
  25. package/dist/Utils.d.ts +51 -0
  26. package/dist/Utils.d.ts.map +1 -1
  27. package/dist/Utils.js +61 -0
  28. package/dist/Utils.js.map +1 -1
  29. package/dist/main.d.ts +12 -0
  30. package/dist/main.d.ts.map +1 -1
  31. package/dist/main.js +41 -8
  32. package/dist/main.js.map +1 -1
  33. package/package.json +10 -8
  34. package/src/HttpApiTransformer.ts +538 -0
  35. package/src/JsonSchemaGenerator.ts +272 -47
  36. package/src/OpenApiGenerator.ts +994 -173
  37. package/src/OpenApiPatch.ts +56 -31
  38. package/src/OpenApiTransformer.ts +89 -12
  39. package/src/ParsedOperation.ts +220 -1
  40. package/src/Utils.ts +61 -0
  41. package/src/main.ts +58 -10
@@ -1,35 +1,109 @@
1
+ /**
2
+ * The `OpenApiGenerator` module orchestrates converting OpenAPI and Swagger
3
+ * documents into generated Effect source.
4
+ *
5
+ * It normalizes Swagger 2.0 input, resolves local references, builds the
6
+ * parsed operation model, registers request and response schemas, applies
7
+ * HttpApi-specific adaptations such as multipart helpers and security metadata,
8
+ * emits warnings for unsupported or lossy OpenAPI features, and then delegates
9
+ * final rendering to the HttpClient or HttpApi code generators.
10
+ *
11
+ * @since 4.0.0
12
+ */
13
+ import * as Context from "effect/Context"
1
14
  import * as Effect from "effect/Effect"
2
15
  import type * as JsonSchema from "effect/JsonSchema"
3
16
  import * as Layer from "effect/Layer"
4
17
  import * as Predicate from "effect/Predicate"
5
- import * as ServiceMap from "effect/ServiceMap"
6
18
  import * as String from "effect/String"
7
- import type { OpenAPISpec, OpenAPISpecMethodName, OpenAPISpecPathItem } from "effect/unstable/httpapi/OpenApi"
19
+ import type { OpenAPISecurityScheme, OpenAPISpec, OpenAPISpecMethodName } from "effect/unstable/httpapi/OpenApi"
8
20
  import SwaggerToOpenApi from "swagger2openapi"
21
+ import * as HttpApiTransformer from "./HttpApiTransformer.ts"
9
22
  import * as JsonSchemaGenerator from "./JsonSchemaGenerator.ts"
10
23
  import * as OpenApiTransformer from "./OpenApiTransformer.ts"
11
24
  import * as ParsedOperation from "./ParsedOperation.ts"
12
25
  import * as Utils from "./Utils.ts"
13
26
 
14
- export class OpenApiGenerator extends ServiceMap.Service<
27
+ /**
28
+ * Service for turning OpenAPI or Swagger specifications into generated Effect
29
+ * HTTP client or HttpApi source code.
30
+ *
31
+ * @category services
32
+ * @since 4.0.0
33
+ */
34
+ export class OpenApiGenerator extends Context.Service<
15
35
  OpenApiGenerator,
16
36
  { readonly generate: (spec: OpenAPISpec, options: OpenApiGenerateOptions) => Effect.Effect<string> }
17
37
  >()("OpenApiGenerator") {}
18
38
 
39
+ /**
40
+ * Output targets supported by the OpenAPI generator.
41
+ *
42
+ * @category models
43
+ * @since 4.0.0
44
+ */
45
+ export type OpenApiGeneratorFormat = "httpclient" | "httpclient-type-only" | "httpapi"
46
+
47
+ /**
48
+ * Stable identifiers for non-fatal OpenAPI generation warnings.
49
+ *
50
+ * @category models
51
+ * @since 4.0.0
52
+ */
53
+ export type OpenApiGeneratorWarningCode =
54
+ | "cookie-parameter-dropped"
55
+ | "additional-tags-dropped"
56
+ | "sse-operation-skipped"
57
+ | "response-headers-ignored"
58
+ | "optional-request-body-approximated"
59
+ | "default-response-remapped"
60
+ | "security-and-downgraded"
61
+ | "no-body-method-request-body-skipped"
62
+ | "naming-collision"
63
+
64
+ /**
65
+ * Describes a non-fatal issue encountered while mapping an OpenAPI operation to
66
+ * generated Effect source.
67
+ *
68
+ * @category models
69
+ * @since 4.0.0
70
+ */
71
+ export interface OpenApiGeneratorWarning {
72
+ readonly code: OpenApiGeneratorWarningCode
73
+ readonly message: string
74
+ readonly path?: string | undefined
75
+ readonly method?: OpenAPISpecMethodName | undefined
76
+ readonly operationId?: string | undefined
77
+ }
78
+
79
+ /**
80
+ * Options that control one OpenAPI generation run.
81
+ *
82
+ * @category options
83
+ * @since 4.0.0
84
+ */
19
85
  export interface OpenApiGenerateOptions {
20
86
  /**
21
- * The name to give to the generated client.
87
+ * The name to give to the generated output.
22
88
  */
23
89
  readonly name: string
24
90
  /**
25
- * When `true`, will **only** generate types based on the provided OpenApi
26
- * specification (without corresponding schemas).
91
+ * The output format to generate.
27
92
  */
28
- readonly typeOnly: boolean
93
+ readonly format: OpenApiGeneratorFormat
29
94
  /**
30
95
  * Hook to transform each JSON Schema node before processing.
31
96
  */
32
97
  readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined
98
+ /**
99
+ * Callback to receive non-fatal generation warnings.
100
+ */
101
+ readonly onWarning?: ((warning: OpenApiGeneratorWarning) => void) | undefined
102
+ }
103
+
104
+ interface HttpApiMultipartSchemaRefs {
105
+ readonly singleFile: string
106
+ readonly files: string
33
107
  }
34
108
 
35
109
  const methodNames: ReadonlyArray<OpenAPISpecMethodName> = [
@@ -43,11 +117,18 @@ const methodNames: ReadonlyArray<OpenAPISpecMethodName> = [
43
117
  "trace"
44
118
  ]
45
119
 
120
+ /**
121
+ * Constructs the OpenAPI generator service implementation.
122
+ *
123
+ * @category constructors
124
+ * @since 4.0.0
125
+ */
46
126
  export const make = Effect.gen(function*() {
47
127
  const generate = Effect.fn(
48
128
  function*(spec: OpenAPISpec, options: OpenApiGenerateOptions) {
49
129
  const generator = JsonSchemaGenerator.make()
50
130
  const openApiTransformer = yield* OpenApiTransformer.OpenApiTransformer
131
+ const emitWarning = makeWarningEmitter(options)
51
132
 
52
133
  // If we receive a Swagger 2.0 spec, convert it to an OpenApi 3.0 spec
53
134
  if (isSwaggerSpec(spec)) {
@@ -63,217 +144,957 @@ export const make = Effect.gen(function*() {
63
144
  return current
64
145
  }
65
146
 
66
- const operations: Array<ParsedOperation.ParsedOperation> = []
147
+ const multipartSchemaRefs = options.format === "httpapi"
148
+ ? makeHttpApiMultipartSchemaRefs(spec.components?.schemas ?? {})
149
+ : undefined
67
150
 
68
- function handlePath(path: string, methods: OpenAPISpecPathItem): void {
69
- for (const method of methodNames) {
70
- const operation = methods[method]
151
+ const parsed = parseOpenApi(spec, generator, resolveRef, options.format, emitWarning, multipartSchemaRefs)
71
152
 
72
- if (Predicate.isUndefined(operation)) {
73
- continue
153
+ // TODO: make a CLI option ?
154
+ const importName = "Schema"
155
+ const source = getDialect(spec)
156
+ const generation = options.format === "httpapi"
157
+ ? generator.generateHttpApi(
158
+ source,
159
+ withHttpApiMultipartSchemas(spec.components?.schemas ?? {}, multipartSchemaRefs),
160
+ {
161
+ onEnter: options.onEnter,
162
+ multipartSchemaRefs
163
+ }
164
+ )
165
+ : generator.generate(
166
+ source,
167
+ spec.components?.schemas ?? {},
168
+ options.format === "httpclient-type-only",
169
+ {
170
+ onEnter: options.onEnter
74
171
  }
172
+ )
75
173
 
76
- const id = operation.operationId
77
- ? Utils.camelize(operation.operationId)
78
- : `${method.toUpperCase()}${path}`
174
+ if (options.format === "httpapi") {
175
+ const needsMultipartImport = generation.includes("Multipart.")
176
+ return String.stripMargin(
177
+ `|${HttpApiTransformer.imports(importName, { multipart: needsMultipartImport })}
178
+ |${generation}
179
+ |${HttpApiTransformer.toImplementation(importName, options.name, parsed)}`
180
+ )
181
+ }
79
182
 
80
- const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary)
183
+ return String.stripMargin(
184
+ `|${openApiTransformer.imports(importName, parsed)}
185
+ |${generation}
186
+ |${openApiTransformer.toImplementation(importName, options.name, parsed)}
187
+ |
188
+ |${openApiTransformer.toTypes(importName, options.name, parsed)}`
189
+ )
190
+ },
191
+ (effect, _, options) =>
192
+ Effect.provideServiceEffect(
193
+ effect,
194
+ OpenApiTransformer.OpenApiTransformer,
195
+ options.format === "httpclient-type-only"
196
+ ? Effect.sync(OpenApiTransformer.makeTransformerTs)
197
+ : Effect.sync(OpenApiTransformer.makeTransformerSchema)
198
+ )
199
+ )
81
200
 
82
- const { pathIds, pathTemplate } = processPath(path)
201
+ return { generate } as const
202
+ })
83
203
 
84
- const op = ParsedOperation.makeDeepMutable({
85
- id,
86
- method,
87
- description,
88
- pathIds,
89
- pathTemplate
90
- })
204
+ type WarningEmitter = (warning: OpenApiGeneratorWarning) => void
91
205
 
92
- const schemaId = Utils.identifier(operation.operationId ?? path)
206
+ const makeWarningEmitter = (options: OpenApiGenerateOptions): WarningEmitter => (warning) => {
207
+ options.onWarning?.(warning)
208
+ }
93
209
 
94
- const validParameters = operation.parameters?.filter((param) => {
95
- return param.in !== "path" && param.in !== "cookie"
96
- }) ?? []
210
+ const parseOpenApi = (
211
+ spec: OpenAPISpec,
212
+ generator: ReturnType<typeof JsonSchemaGenerator.make>,
213
+ resolveRef: (ref: string) => unknown,
214
+ format: OpenApiGeneratorFormat,
215
+ emitWarning: WarningEmitter,
216
+ multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined
217
+ ): ParsedOperation.ParsedOpenApi => {
218
+ const operations: Array<ParsedOperation.ParsedOperation> = []
219
+ const reservedSchemaNames = new Set<string>(Object.keys(spec.components?.schemas ?? {}))
220
+ const isHttpApi = format === "httpapi"
221
+ const securitySchemes = isHttpApi ? parseSecuritySchemes(spec, resolveRef) : []
97
222
 
98
- if (validParameters.length > 0) {
99
- const schema = {
100
- type: "object" as JsonSchema.Type,
101
- properties: {} as Record<string, any>,
102
- required: [] as Array<string>,
103
- additionalProperties: false
104
- }
223
+ const addSchema = (
224
+ baseName: string,
225
+ schema: JsonSchema.JsonSchema,
226
+ operation: ParsedOperation.ParsedOperation
227
+ ): string => {
228
+ let candidate = baseName
229
+ let index = 2
230
+ while (reservedSchemaNames.has(candidate)) {
231
+ candidate = `${baseName}${index}`
232
+ index += 1
233
+ }
234
+ if (candidate !== baseName) {
235
+ warnForOperation(emitWarning, operation, {
236
+ code: "naming-collision",
237
+ message: `Schema name "${baseName}" collided with an existing name and was renamed to "${candidate}".`
238
+ })
239
+ }
240
+ reservedSchemaNames.add(candidate)
241
+ return generator.addSchema(candidate, schema)
242
+ }
105
243
 
106
- for (let parameter of validParameters) {
107
- if ("$ref" in parameter) {
108
- parameter = resolveRef(parameter.$ref as string)
109
- }
110
-
111
- if (parameter.in === "path") {
112
- continue
113
- }
114
-
115
- const paramSchema = parameter.schema
116
- const added: Array<string> = []
117
- if ("properties" in paramSchema && Predicate.isObject(paramSchema.properties)) {
118
- const required = "required" in paramSchema
119
- ? paramSchema.required as Array<string>
120
- : []
121
-
122
- for (const [name, propSchema] of Object.entries(paramSchema.properties)) {
123
- const adjustedName = `${parameter.name}[${name}]`
124
- schema.properties[adjustedName] = propSchema
125
- if (required.includes(name)) {
126
- schema.required.push(adjustedName)
127
- }
128
- added.push(adjustedName)
129
- }
130
- } else {
131
- schema.properties[parameter.name] = parameter.schema
132
- if (parameter.required) {
133
- schema.required.push(parameter.name)
134
- }
135
- added.push(parameter.name)
136
- }
137
-
138
- if (parameter.in === "query") {
139
- Utils.spreadElementsInto(added, op.urlParams)
140
- } else if (parameter.in === "header") {
141
- Utils.spreadElementsInto(added, op.headers)
142
- } else if (parameter.in === "cookie") {
143
- Utils.spreadElementsInto(added, op.cookies)
144
- }
145
- }
244
+ for (const [path, methods] of Object.entries(spec.paths)) {
245
+ for (const method of methodNames) {
246
+ const operation = methods[method]
146
247
 
147
- op.params = generator.addSchema(
148
- `${schemaId}Params`,
149
- schema
150
- )
248
+ if (Predicate.isUndefined(operation)) {
249
+ continue
250
+ }
151
251
 
152
- op.paramsOptional = !schema.required || schema.required.length === 0
153
- }
252
+ const id = operation.operationId
253
+ ? Utils.camelize(operation.operationId)
254
+ : `${method.toUpperCase()}${path}`
154
255
 
155
- if (Predicate.isNotUndefined(operation.requestBody?.content?.["application/json"]?.schema)) {
156
- op.payload = generator.addSchema(
157
- `${schemaId}RequestJson`,
158
- operation.requestBody.content["application/json"].schema
159
- )
160
- }
256
+ const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary)
161
257
 
162
- if (Predicate.isNotUndefined(operation.requestBody?.content?.["multipart/form-data"]?.schema)) {
163
- op.payload = generator.addSchema(
164
- `${schemaId}RequestFormData`,
165
- operation.requestBody.content["multipart/form-data"].schema
166
- )
167
- op.payloadFormData = true
258
+ const { pathIds, pathTemplate } = processPath(path)
259
+
260
+ const op = ParsedOperation.makeDeepMutable({
261
+ id,
262
+ method,
263
+ description,
264
+ pathIds,
265
+ pathTemplate
266
+ })
267
+ op.path = path
268
+ op.operationId = Utils.nonEmptyString(operation.operationId)
269
+ op.tags = [...(operation.tags ?? [])]
270
+ if (isHttpApi && op.tags.length > 1) {
271
+ warnForOperation(emitWarning, op, {
272
+ code: "additional-tags-dropped",
273
+ message: `Additional tags (${op.tags.slice(1).join(", ")}) were dropped. Only the first tag ("${
274
+ op.tags[0]
275
+ }") is used for grouping.`
276
+ })
277
+ }
278
+ op.metadata = {
279
+ summary: Utils.nonEmptyString(operation.summary),
280
+ description: Utils.nonEmptyString(operation.description),
281
+ deprecated: operation.deprecated === true,
282
+ externalDocs: operation.externalDocs
283
+ }
284
+ op.effectiveSecurity = cloneSecurityRequirements(operation.security ?? spec.security ?? [])
285
+ if (isHttpApi) {
286
+ warnForAndSecurityRequirements(emitWarning, op)
287
+ }
288
+
289
+ const schemaId = Utils.identifier(operation.operationId ?? path)
290
+
291
+ const pathParameters = Predicate.isObject(methods) && Array.isArray((methods as any).parameters)
292
+ ? (methods as any).parameters as ReadonlyArray<unknown>
293
+ : undefined
294
+ const parameters = resolveOperationParameters(
295
+ pathParameters,
296
+ Array.isArray(operation.parameters) ? operation.parameters : undefined,
297
+ resolveRef
298
+ )
299
+ for (const parameter of parameters) {
300
+ const parsedParameter: ParsedOperation.ParsedOperationParameter = {
301
+ name: parameter.name,
302
+ in: parameter.in,
303
+ required: parameter.required === true,
304
+ description: Utils.nonEmptyString(parameter.description),
305
+ schema: parameter.schema
306
+ }
307
+ switch (parameter.in) {
308
+ case "path": {
309
+ op.parameters.path.push(parsedParameter)
310
+ break
168
311
  }
312
+ case "query": {
313
+ op.parameters.query.push(parsedParameter)
314
+ break
315
+ }
316
+ case "header": {
317
+ op.parameters.header.push(parsedParameter)
318
+ break
319
+ }
320
+ case "cookie": {
321
+ op.parameters.cookie.push(parsedParameter)
322
+ warnForOperation(emitWarning, op, {
323
+ code: "cookie-parameter-dropped",
324
+ message:
325
+ `Cookie parameter "${parameter.name}" was dropped because non-security cookie parameters are not supported.`
326
+ })
327
+ break
328
+ }
329
+ }
330
+ }
169
331
 
170
- let defaultSchema: string | undefined
171
- for (const entry of Object.entries(operation.responses ?? {})) {
172
- const status = entry[0]
173
- let response = entry[1]
332
+ const requestBody = resolveReference(operation.requestBody, resolveRef)
333
+ if (isHttpApi && !methodSupportsRequestBody(op.method) && Predicate.isObject(requestBody)) {
334
+ warnForOperation(emitWarning, op, {
335
+ code: "no-body-method-request-body-skipped",
336
+ message: `Operation was skipped because ${op.method.toUpperCase()} does not support request bodies.`
337
+ })
338
+ continue
339
+ }
174
340
 
175
- while ("$ref" in response) {
176
- response = resolveRef(response.$ref as string)
177
- }
341
+ const resolvedResponses = Object.entries(operation.responses ?? {}).map(
342
+ ([status, response]) => [status, resolveReference(response, resolveRef)] as const
343
+ )
344
+ const hasExplicitSuccessResponse = resolvedResponses.some(([status]) => {
345
+ if (!/^\d{3}$/.test(status)) {
346
+ return false
347
+ }
348
+ return Number(status) < 400
349
+ })
178
350
 
179
- if (Predicate.isNotUndefined(response.content?.["application/json"]?.schema)) {
180
- const schemaName = generator.addSchema(
181
- `${schemaId}${status}`,
182
- response.content["application/json"].schema
183
- )
351
+ if (isHttpApi && hasSuccessfulSseResponse(resolvedResponses, hasExplicitSuccessResponse)) {
352
+ warnForOperation(emitWarning, op, {
353
+ code: "sse-operation-skipped",
354
+ message:
355
+ "Operation was skipped because successful text/event-stream responses are not supported in HttpApi generation."
356
+ })
357
+ continue
358
+ }
184
359
 
185
- if (status === "default") {
186
- defaultSchema = schemaName
187
- continue
188
- }
189
-
190
- const statusLower = status.toLowerCase()
191
- const statusMajorNumber = Number(status[0])
192
- if (Number.isNaN(statusMajorNumber)) {
193
- continue
194
- }
195
- if (statusMajorNumber < 4) {
196
- op.successSchemas.set(statusLower, schemaName)
197
- } else {
198
- op.errorSchemas.set(statusLower, schemaName)
199
- }
200
- }
360
+ const validParameters = parameters.filter((parameter) => parameter.in !== "path" && parameter.in !== "cookie")
201
361
 
202
- // Handle SSE streaming responses (text/event-stream)
203
- if (
204
- Predicate.isUndefined(op.sseSchema) &&
205
- Predicate.isNotUndefined(response.content?.["text/event-stream"]?.schema)
206
- ) {
207
- const statusMajorNumber = Number(status[0])
208
- if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
209
- op.sseSchema = generator.addSchema(
210
- `${schemaId}${status}Sse`,
211
- response.content["text/event-stream"].schema
212
- )
213
- }
214
- }
362
+ const combinedParameterSchema = buildParameterSchema(validParameters, (parameter, added) => {
363
+ if (parameter.in === "query") {
364
+ Utils.spreadElementsInto(added, op.urlParams)
365
+ } else if (parameter.in === "header") {
366
+ Utils.spreadElementsInto(added, op.headers)
367
+ } else if (parameter.in === "cookie") {
368
+ Utils.spreadElementsInto(added, op.cookies)
369
+ }
370
+ })
371
+
372
+ if (combinedParameterSchema !== undefined) {
373
+ op.params = addSchema(`${schemaId}Params`, combinedParameterSchema.schema, op)
374
+ op.paramsOptional = combinedParameterSchema.optional
375
+ }
376
+
377
+ if (isHttpApi) {
378
+ const pathParameterSchema = buildParameterSchema(op.parameters.path)
379
+ if (pathParameterSchema !== undefined) {
380
+ op.pathSchema = addSchema(`${schemaId}PathParams`, pathParameterSchema.schema, op)
381
+ }
382
+
383
+ const queryParameterSchema = buildParameterSchema(op.parameters.query)
384
+ if (queryParameterSchema !== undefined) {
385
+ op.querySchema = addSchema(`${schemaId}Query`, queryParameterSchema.schema, op)
386
+ op.querySchemaOptional = queryParameterSchema.optional
387
+ }
388
+
389
+ const headerParameterSchema = buildParameterSchema(op.parameters.header)
390
+ if (headerParameterSchema !== undefined) {
391
+ op.headersSchema = addSchema(`${schemaId}Headers`, headerParameterSchema.schema, op)
392
+ op.headersSchemaOptional = headerParameterSchema.optional
393
+ }
394
+ }
395
+
396
+ if (Predicate.isNotUndefined(requestBody) && Predicate.isObject(requestBody)) {
397
+ const content = Predicate.isObject(requestBody.content)
398
+ ? requestBody.content as Record<string, any>
399
+ : {}
400
+ const requestSchemaNames = new Map<string, string>()
401
+ op.requestBody = {
402
+ required: requestBody.required === true,
403
+ contentTypes: Object.keys(content)
404
+ }
405
+ if (isHttpApi && requestBody.required === false) {
406
+ warnForOperation(emitWarning, op, {
407
+ code: "optional-request-body-approximated",
408
+ message: "Optional request body was approximated by adding a no-content payload alternative."
409
+ })
410
+ }
411
+
412
+ if (Predicate.isNotUndefined(content["application/json"]?.schema)) {
413
+ op.payload = addSchema(`${schemaId}RequestJson`, content["application/json"].schema, op)
414
+ requestSchemaNames.set("application/json", op.payload)
415
+ }
416
+
417
+ if (Predicate.isNotUndefined(content["multipart/form-data"]?.schema)) {
418
+ op.payload = addSchema(
419
+ `${schemaId}RequestFormData`,
420
+ transformMultipartSchema(content["multipart/form-data"].schema, multipartSchemaRefs, resolveRef),
421
+ op
422
+ )
423
+ op.payloadFormData = true
424
+ requestSchemaNames.set("multipart/form-data", op.payload)
425
+ }
215
426
 
216
- // Handle binary streaming responses (application/octet-stream)
217
- if (Predicate.isNotUndefined(response.content?.["application/octet-stream"])) {
218
- const statusMajorNumber = Number(status[0])
219
- if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
220
- op.binaryResponse = true
221
- }
427
+ if (Predicate.isNotUndefined(content["application/x-www-form-urlencoded"]?.schema)) {
428
+ op.payload = addSchema(
429
+ `${schemaId}RequestFormUrlEncoded`,
430
+ content["application/x-www-form-urlencoded"].schema,
431
+ op
432
+ )
433
+ op.payloadFormUrlEncoded = true
434
+ requestSchemaNames.set("application/x-www-form-urlencoded", op.payload)
435
+ }
436
+
437
+ if (isHttpApi) {
438
+ const representableRequestBody: Array<ParsedOperation.ParsedOperationMediaTypeSchema> = []
439
+ for (const [contentType, mediaType] of Object.entries(content)) {
440
+ if (!Predicate.isObject(mediaType) || Predicate.isUndefined(mediaType.schema)) {
441
+ continue
222
442
  }
443
+ const encoding = getRequestMediaTypeEncoding(contentType)
444
+ if (encoding === undefined) {
445
+ continue
446
+ }
447
+ let schemaName = requestSchemaNames.get(contentType)
448
+ if (schemaName === undefined) {
449
+ const schema = encoding === "multipart"
450
+ ? transformMultipartSchema(mediaType.schema as JsonSchema.JsonSchema, multipartSchemaRefs, resolveRef)
451
+ : mediaType.schema as JsonSchema.JsonSchema
452
+ schemaName = addSchema(
453
+ `${schemaId}Request${mediaTypeToSuffix(contentType)}`,
454
+ schema,
455
+ op
456
+ )
457
+ requestSchemaNames.set(contentType, schemaName)
458
+ }
459
+ representableRequestBody.push({
460
+ contentType,
461
+ encoding,
462
+ schema: schemaName
463
+ })
464
+ }
465
+ op.requestBodyRepresentable = representableRequestBody
466
+ }
467
+ }
468
+
469
+ let defaultSchema: string | undefined
470
+ for (const [status, response] of resolvedResponses) {
471
+ if (!Predicate.isObject(response)) {
472
+ continue
473
+ }
223
474
 
224
- if (Predicate.isUndefined(response.content)) {
225
- if (status !== "default") {
226
- op.voidSchemas.add(status.toLowerCase())
227
- }
475
+ const parsedStatus = isHttpApi
476
+ ? remapDefaultResponseStatusForHttpApi(status, hasExplicitSuccessResponse)
477
+ : status
478
+ if (isHttpApi && status === "default") {
479
+ warnForOperation(emitWarning, op, {
480
+ code: "default-response-remapped",
481
+ message: `Default response was remapped to status ${parsedStatus} for HttpApi generation.`
482
+ })
483
+ }
484
+
485
+ const content = Predicate.isObject(response.content)
486
+ ? response.content as Record<string, any>
487
+ : undefined
488
+ if (isHttpApi && Predicate.isNotUndefined(response.headers)) {
489
+ warnForOperation(emitWarning, op, {
490
+ code: "response-headers-ignored",
491
+ message: `Response headers on status ${status} were ignored in HttpApi generation.`
492
+ })
493
+ }
494
+ const representable: Array<ParsedOperation.ParsedOperationMediaTypeSchema> = []
495
+
496
+ let jsonSchemaName: string | undefined
497
+ const jsonResponseSchema = content?.["application/json"]?.schema
498
+ if (Predicate.isNotUndefined(jsonResponseSchema)) {
499
+ jsonSchemaName = addSchema(`${schemaId}${status}`, jsonResponseSchema, op)
500
+ if (isHttpApi) {
501
+ representable.push({
502
+ contentType: "application/json",
503
+ encoding: "json",
504
+ schema: jsonSchemaName
505
+ })
506
+ }
507
+ }
508
+
509
+ if (isHttpApi) {
510
+ for (const [contentType, mediaType] of Object.entries(content ?? {})) {
511
+ if (contentType === "application/json") {
512
+ continue
513
+ }
514
+ if (!Predicate.isObject(mediaType) || Predicate.isUndefined(mediaType.schema)) {
515
+ continue
516
+ }
517
+ const encoding = getResponseMediaTypeEncoding(contentType)
518
+ if (encoding === undefined) {
519
+ continue
228
520
  }
521
+ const schemaName = addSchema(
522
+ `${schemaId}${status}${mediaTypeToSuffix(contentType)}`,
523
+ mediaType.schema as JsonSchema.JsonSchema,
524
+ op
525
+ )
526
+ representable.push({
527
+ contentType,
528
+ encoding,
529
+ schema: schemaName
530
+ })
229
531
  }
532
+ }
533
+
534
+ const isEmptyResponse = Predicate.isUndefined(content) || Object.keys(content).length === 0
535
+ const parsedResponse = {
536
+ status: parsedStatus,
537
+ description: Utils.nonEmptyString(response.description),
538
+ contentTypes: Predicate.isNotUndefined(content) ? Object.keys(content) : [],
539
+ hasHeaders: Predicate.isNotUndefined(response.headers),
540
+ isEmpty: isEmptyResponse,
541
+ representable
542
+ }
543
+ op.responses.push(parsedResponse)
544
+ if (status === "default") {
545
+ op.defaultResponse = parsedResponse
546
+ }
547
+
548
+ if (Predicate.isNotUndefined(jsonSchemaName)) {
549
+ const schemaName = jsonSchemaName
230
550
 
231
- if (op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
232
- op.successSchemas.set("2xx", defaultSchema)
551
+ if (status === "default" && !isHttpApi) {
552
+ defaultSchema = schemaName
553
+ continue
554
+ }
555
+
556
+ const statusLower = parsedStatus.toLowerCase()
557
+ const statusMajorNumber = Number(parsedStatus[0])
558
+ if (Number.isNaN(statusMajorNumber)) {
559
+ continue
560
+ }
561
+ if (statusMajorNumber < 4) {
562
+ op.successSchemas.set(statusLower, schemaName)
563
+ } else {
564
+ op.errorSchemas.set(statusLower, schemaName)
565
+ }
566
+ }
567
+
568
+ const sseResponseSchema = content?.["text/event-stream"]?.schema
569
+ if (Predicate.isUndefined(op.sseSchema) && Predicate.isNotUndefined(sseResponseSchema)) {
570
+ const statusMajorNumber = Number(parsedStatus[0])
571
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
572
+ op.sseSchema = addSchema(`${schemaId}${status}Sse`, sseResponseSchema, op)
233
573
  }
574
+ }
234
575
 
235
- operations.push(op)
576
+ if (Predicate.isNotUndefined(content?.["application/octet-stream"])) {
577
+ const statusMajorNumber = Number(parsedStatus[0])
578
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
579
+ op.binaryResponse = true
580
+ }
581
+ }
582
+
583
+ if (isEmptyResponse) {
584
+ if (parsedStatus !== "default") {
585
+ op.voidSchemas.add(parsedStatus.toLowerCase())
586
+ }
236
587
  }
237
588
  }
238
589
 
239
- for (const [path, methods] of Object.entries(spec.paths)) {
240
- handlePath(path, methods)
590
+ if (!isHttpApi && op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
591
+ op.successSchemas.set("2xx", defaultSchema)
592
+ warnForOperation(emitWarning, op, {
593
+ code: "default-response-remapped",
594
+ message: "Default response was remapped to 2xx for the current HttpClient outputs."
595
+ })
241
596
  }
242
597
 
243
- // TODO: make a CLI option ?
244
- const importName = "Schema"
245
- const source = getDialect(spec)
246
- const generation = generator.generate(source, spec.components?.schemas ?? {}, options.typeOnly, {
247
- onEnter: options.onEnter
248
- })
598
+ operations.push(op)
599
+ }
600
+ }
249
601
 
250
- return String.stripMargin(
251
- `|${openApiTransformer.imports(importName, operations)}
252
- |${generation}
253
- |${openApiTransformer.toImplementation(importName, options.name, operations)}
254
- |
255
- |${openApiTransformer.toTypes(importName, options.name, operations)}`
256
- )
602
+ return {
603
+ metadata: {
604
+ title: spec.info.title,
605
+ version: spec.info.version,
606
+ summary: Utils.nonEmptyString(spec.info.summary),
607
+ description: Utils.nonEmptyString(spec.info.description),
608
+ license: spec.info.license,
609
+ servers: spec.servers
257
610
  },
258
- (effect, _, options) =>
259
- Effect.provideServiceEffect(
260
- effect,
261
- OpenApiTransformer.OpenApiTransformer,
262
- options.typeOnly
263
- ? Effect.sync(OpenApiTransformer.makeTransformerTs)
264
- : Effect.sync(OpenApiTransformer.makeTransformerSchema)
265
- )
611
+ tags: (spec.tags ?? []).map((tag) => ({
612
+ name: tag.name,
613
+ description: Utils.nonEmptyString(tag.description),
614
+ externalDocs: tag.externalDocs
615
+ })),
616
+ securitySchemes,
617
+ operations
618
+ }
619
+ }
620
+
621
+ interface OpenApiParameter {
622
+ readonly name: string
623
+ readonly in: "path" | "query" | "header" | "cookie"
624
+ readonly required: boolean
625
+ readonly schema: {}
626
+ readonly description?: string | undefined
627
+ }
628
+
629
+ const isOpenApiParameter = (parameter: unknown): parameter is OpenApiParameter => {
630
+ if (!Predicate.isObject(parameter)) {
631
+ return false
632
+ }
633
+ return (
634
+ typeof parameter.name === "string" &&
635
+ (parameter.in === "path" || parameter.in === "query" || parameter.in === "header" || parameter.in === "cookie")
266
636
  )
637
+ }
267
638
 
268
- return { generate } as const
269
- })
639
+ const resolveOperationParameters = (
640
+ pathParameters: ReadonlyArray<unknown> | undefined,
641
+ operationParameters: ReadonlyArray<unknown> | undefined,
642
+ resolveRef: (ref: string) => unknown
643
+ ): Array<OpenApiParameter> => {
644
+ const resolved = new Map<string, OpenApiParameter>()
645
+ const add = (parameter: unknown): void => {
646
+ const current = resolveReference(parameter, resolveRef)
647
+ if (!isOpenApiParameter(current)) {
648
+ return
649
+ }
650
+
651
+ const key = `${current.in}:${current.name}`
652
+ if (resolved.has(key)) {
653
+ resolved.delete(key)
654
+ }
655
+ resolved.set(key, current)
656
+ }
657
+
658
+ for (const parameter of pathParameters ?? []) {
659
+ add(parameter)
660
+ }
661
+
662
+ for (const parameter of operationParameters ?? []) {
663
+ add(parameter)
664
+ }
665
+
666
+ return [...resolved.values()]
667
+ }
668
+
669
+ const buildParameterSchema = <
670
+ Parameter extends {
671
+ readonly name: string
672
+ readonly required: boolean
673
+ readonly schema: {}
674
+ readonly in?: "path" | "query" | "header" | "cookie" | undefined
675
+ }
676
+ >(
677
+ parameters: ReadonlyArray<Parameter>,
678
+ onAdded?: ((parameter: Parameter, added: Array<string>) => void) | undefined
679
+ ): {
680
+ readonly schema: JsonSchema.JsonSchema
681
+ readonly optional: boolean
682
+ } | undefined => {
683
+ if (parameters.length === 0) {
684
+ return
685
+ }
686
+
687
+ const schema = {
688
+ type: "object" as JsonSchema.Type,
689
+ properties: {} as Record<string, JsonSchema.JsonSchema>,
690
+ required: [] as Array<string>,
691
+ additionalProperties: false
692
+ }
693
+
694
+ for (const parameter of parameters) {
695
+ const paramSchema = parameter.schema as any
696
+ const added: Array<string> = []
697
+ if (
698
+ Predicate.isObject(paramSchema) &&
699
+ "properties" in paramSchema &&
700
+ Predicate.isObject(paramSchema.properties)
701
+ ) {
702
+ const required = "required" in paramSchema
703
+ ? paramSchema.required as Array<string>
704
+ : []
705
+
706
+ for (const [name, propertySchema] of Object.entries(paramSchema.properties)) {
707
+ const adjustedName = `${parameter.name}[${name}]`
708
+ schema.properties[adjustedName] = propertySchema as JsonSchema.JsonSchema
709
+ if (required.includes(name)) {
710
+ schema.required.push(adjustedName)
711
+ }
712
+ added.push(adjustedName)
713
+ }
714
+ } else {
715
+ schema.properties[parameter.name] = parameter.schema as JsonSchema.JsonSchema
716
+ if (parameter.required) {
717
+ schema.required.push(parameter.name)
718
+ }
719
+ added.push(parameter.name)
720
+ }
721
+
722
+ onAdded?.(parameter, added)
723
+ }
724
+
725
+ return {
726
+ schema,
727
+ optional: schema.required.length === 0
728
+ }
729
+ }
730
+
731
+ const mediaTypeToSuffix = (contentType: string): string => {
732
+ const normalized = contentType.toLowerCase()
733
+ switch (normalized) {
734
+ case "application/json":
735
+ return "Json"
736
+ case "multipart/form-data":
737
+ return "FormData"
738
+ case "application/x-www-form-urlencoded":
739
+ return "FormUrlEncoded"
740
+ case "text/plain":
741
+ return "Text"
742
+ case "application/octet-stream":
743
+ return "Binary"
744
+ }
745
+ const suffix = Utils.identifier(contentType)
746
+ return suffix.length > 0 ? suffix : "Body"
747
+ }
748
+
749
+ const makeHttpApiMultipartSchemaRefs = (definitions: JsonSchema.Definitions): HttpApiMultipartSchemaRefs => {
750
+ const names = new Set(Object.keys(definitions))
751
+ const allocate = (base: string): string => {
752
+ let candidate = base
753
+ let index = 2
754
+ while (names.has(candidate)) {
755
+ candidate = `${base}${index}`
756
+ index += 1
757
+ }
758
+ names.add(candidate)
759
+ return candidate
760
+ }
761
+ return {
762
+ singleFile: allocate("__HttpApiMultipartSingleFile"),
763
+ files: allocate("__HttpApiMultipartFiles")
764
+ }
765
+ }
766
+
767
+ const toDefinitionRef = (name: string): string => `#/$defs/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`
768
+
769
+ const withHttpApiMultipartSchemas = (
770
+ definitions: JsonSchema.Definitions,
771
+ multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined
772
+ ): JsonSchema.Definitions => {
773
+ if (multipartSchemaRefs === undefined) {
774
+ return definitions
775
+ }
776
+ return {
777
+ ...definitions,
778
+ [multipartSchemaRefs.singleFile]: {
779
+ type: "string",
780
+ format: "binary"
781
+ },
782
+ [multipartSchemaRefs.files]: {
783
+ type: "array",
784
+ items: {
785
+ $ref: toDefinitionRef(multipartSchemaRefs.singleFile)
786
+ }
787
+ }
788
+ }
789
+ }
790
+
791
+ const transformMultipartSchema = (
792
+ schema: JsonSchema.JsonSchema,
793
+ multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined,
794
+ resolveRef: (ref: string) => unknown
795
+ ): JsonSchema.JsonSchema => {
796
+ if (multipartSchemaRefs === undefined) {
797
+ return schema
798
+ }
799
+
800
+ const singleFileRef = toDefinitionRef(multipartSchemaRefs.singleFile)
801
+ const filesRef = toDefinitionRef(multipartSchemaRefs.files)
802
+ const cache = new Map<string, unknown>()
803
+ const stack = new Set<string>()
804
+
805
+ const visit = (value: unknown): unknown => {
806
+ if (Array.isArray(value)) {
807
+ return value.map(visit)
808
+ }
809
+ if (!Predicate.isObject(value)) {
810
+ return value
811
+ }
812
+
813
+ if (typeof value.$ref === "string" && value.$ref.startsWith("#/components/schemas/")) {
814
+ const cached = cache.get(value.$ref)
815
+ if (cached !== undefined) {
816
+ return cached
817
+ }
818
+ if (stack.has(value.$ref)) {
819
+ return value
820
+ }
821
+ stack.add(value.$ref)
822
+ const transformed = visit(resolveSchemaReference(value.$ref, resolveRef))
823
+ stack.delete(value.$ref)
824
+ cache.set(value.$ref, transformed)
825
+ return transformed
826
+ }
827
+
828
+ if (isMultipartBinaryFile(value)) {
829
+ return { $ref: singleFileRef }
830
+ }
831
+
832
+ const out: Record<string, unknown> = {}
833
+ for (const [key, current] of Object.entries(value)) {
834
+ out[key] = visit(current)
835
+ }
836
+
837
+ if (isMultipartBinaryFiles(out, singleFileRef)) {
838
+ return { $ref: filesRef }
839
+ }
840
+
841
+ return out
842
+ }
843
+
844
+ return visit(schema) as JsonSchema.JsonSchema
845
+ }
846
+
847
+ const resolveSchemaReference = (ref: string, resolveRef: (ref: string) => unknown): unknown => {
848
+ let current: unknown = { $ref: ref }
849
+ const seen = new Set<string>()
850
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
851
+ if (seen.has(current.$ref)) {
852
+ return current
853
+ }
854
+ seen.add(current.$ref)
855
+ current = resolveRef(current.$ref)
856
+ }
857
+ return current
858
+ }
859
+
860
+ const isMultipartBinaryFile = (value: unknown): value is JsonSchema.JsonSchema =>
861
+ Predicate.isObject(value) &&
862
+ value.type === "string" &&
863
+ (
864
+ (typeof value.format === "string" && value.format.toLowerCase() === "binary") ||
865
+ (typeof value.contentEncoding === "string" && value.contentEncoding.toLowerCase() === "binary")
866
+ )
867
+
868
+ const isMultipartBinaryFiles = (value: Record<string, unknown>, singleFileRef: string): boolean => {
869
+ if (value.type !== "array") {
870
+ return false
871
+ }
872
+ const items = value.items
873
+ return isMultipartBinaryFile(items) || (Predicate.isObject(items) && items.$ref === singleFileRef)
874
+ }
875
+
876
+ const isJsonMediaType = (contentType: string): boolean =>
877
+ contentType === "application/json" ||
878
+ (contentType.startsWith("application/") && contentType.endsWith("+json"))
879
+
880
+ const isTextMediaType = (contentType: string): boolean => contentType.startsWith("text/")
881
+
882
+ const isBinaryMediaType = (contentType: string): boolean =>
883
+ contentType === "application/octet-stream" ||
884
+ (contentType.startsWith("application/") && (contentType.includes("binary") || contentType.endsWith("+octet-stream")))
885
+
886
+ const getRequestMediaTypeEncoding = (
887
+ contentType: string
888
+ ): ParsedOperation.ParsedOperationMediaTypeEncoding | undefined => {
889
+ const normalized = contentType.toLowerCase()
890
+ if (isJsonMediaType(normalized)) {
891
+ return "json"
892
+ }
893
+ if (normalized === "multipart/form-data") {
894
+ return "multipart"
895
+ }
896
+ if (normalized === "application/x-www-form-urlencoded") {
897
+ return "form-url-encoded"
898
+ }
899
+ if (isTextMediaType(normalized)) {
900
+ return "text"
901
+ }
902
+ if (isBinaryMediaType(normalized)) {
903
+ return "binary"
904
+ }
905
+ return
906
+ }
907
+
908
+ const getResponseMediaTypeEncoding = (
909
+ contentType: string
910
+ ): ParsedOperation.ParsedOperationMediaTypeEncoding | undefined => {
911
+ const normalized = contentType.toLowerCase()
912
+ if (isJsonMediaType(normalized)) {
913
+ return "json"
914
+ }
915
+ if (normalized === "application/x-www-form-urlencoded") {
916
+ return "form-url-encoded"
917
+ }
918
+ if (isTextMediaType(normalized)) {
919
+ return "text"
920
+ }
921
+ if (isBinaryMediaType(normalized)) {
922
+ return "binary"
923
+ }
924
+ return
925
+ }
926
+
927
+ const resolveReference = (input: unknown, resolveRef: (ref: string) => unknown): any => {
928
+ let current = input
929
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
930
+ current = resolveRef(current.$ref)
931
+ }
932
+ return current
933
+ }
934
+
935
+ const cloneSecurityRequirements = (
936
+ security: ReadonlyArray<Record<string, ReadonlyArray<string>>>
937
+ ): Array<ParsedOperation.ParsedOperationSecurityRequirement> =>
938
+ security.map((requirement) =>
939
+ Object.fromEntries(
940
+ Object.entries(requirement).map(([name, scopes]) => [name, [...scopes]])
941
+ )
942
+ )
943
+
944
+ const parseSecuritySchemes = (
945
+ spec: OpenAPISpec,
946
+ resolveRef: (ref: string) => unknown
947
+ ): Array<ParsedOperation.ParsedOpenApiSecurityScheme> => {
948
+ const securitySchemes = spec.components?.securitySchemes ?? {}
949
+ const parsed: Array<ParsedOperation.ParsedOpenApiSecurityScheme> = []
950
+
951
+ for (const [name, value] of Object.entries(securitySchemes)) {
952
+ const scheme = resolveReference(value, resolveRef) as OpenAPISecurityScheme | undefined
953
+ if (!Predicate.isObject(scheme)) {
954
+ continue
955
+ }
956
+
957
+ if (scheme.type === "http" && typeof scheme.scheme === "string") {
958
+ const normalizedScheme = scheme.scheme.toLowerCase()
959
+ if (normalizedScheme === "basic") {
960
+ parsed.push({
961
+ name,
962
+ type: "basic",
963
+ description: Utils.nonEmptyString(scheme.description),
964
+ bearerFormat: undefined,
965
+ scheme: undefined,
966
+ key: undefined,
967
+ in: undefined
968
+ })
969
+ } else if (normalizedScheme === "bearer") {
970
+ parsed.push({
971
+ name,
972
+ type: "bearer",
973
+ description: Utils.nonEmptyString(scheme.description),
974
+ bearerFormat: Utils.nonEmptyString(scheme.bearerFormat),
975
+ scheme: undefined,
976
+ key: undefined,
977
+ in: undefined
978
+ })
979
+ } else {
980
+ parsed.push({
981
+ name,
982
+ type: "http",
983
+ description: Utils.nonEmptyString(scheme.description),
984
+ bearerFormat: Utils.nonEmptyString(scheme.bearerFormat),
985
+ scheme: scheme.scheme,
986
+ key: undefined,
987
+ in: undefined
988
+ })
989
+ }
990
+ continue
991
+ }
992
+
993
+ if (
994
+ scheme.type === "apiKey" &&
995
+ (scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie") &&
996
+ typeof scheme.name === "string"
997
+ ) {
998
+ parsed.push({
999
+ name,
1000
+ type: "apiKey",
1001
+ description: Utils.nonEmptyString(scheme.description),
1002
+ bearerFormat: undefined,
1003
+ scheme: undefined,
1004
+ key: scheme.name,
1005
+ in: scheme.in
1006
+ })
1007
+ }
1008
+ }
1009
+
1010
+ return parsed
1011
+ }
1012
+
1013
+ const warnForAndSecurityRequirements = (
1014
+ emitWarning: WarningEmitter,
1015
+ operation: ParsedOperation.ParsedOperation
1016
+ ): void => {
1017
+ if (operation.effectiveSecurity.some((requirement) => Object.keys(requirement).length === 0)) {
1018
+ return
1019
+ }
1020
+ for (const requirement of operation.effectiveSecurity) {
1021
+ const schemes = Object.keys(requirement)
1022
+ if (schemes.length <= 1) {
1023
+ continue
1024
+ }
1025
+ warnForOperation(emitWarning, operation, {
1026
+ code: "security-and-downgraded",
1027
+ message: `Security requirement requiring all of [${
1028
+ schemes.join(", ")
1029
+ }] was downgraded to a placeholder middleware.`
1030
+ })
1031
+ }
1032
+ }
1033
+
1034
+ const hasSuccessfulSseResponse = (
1035
+ responses: ReadonlyArray<readonly [string, unknown]>,
1036
+ hasExplicitSuccessResponse: boolean
1037
+ ): boolean => {
1038
+ for (const [status, response] of responses) {
1039
+ if (!Predicate.isObject(response)) {
1040
+ continue
1041
+ }
1042
+ const content = Predicate.isObject(response.content)
1043
+ ? response.content as Record<string, any>
1044
+ : undefined
1045
+ if (Predicate.isUndefined(content?.["text/event-stream"]?.schema)) {
1046
+ continue
1047
+ }
1048
+
1049
+ const remappedStatus = remapDefaultResponseStatusForHttpApi(status, hasExplicitSuccessResponse)
1050
+ const statusCode = Number(remappedStatus)
1051
+ if (!Number.isNaN(statusCode) && statusCode < 400) {
1052
+ return true
1053
+ }
1054
+ }
1055
+ return false
1056
+ }
1057
+
1058
+ const remapDefaultResponseStatusForHttpApi = (status: string, hasExplicitSuccessResponse: boolean): string =>
1059
+ status === "default" ? (hasExplicitSuccessResponse ? "500" : "200") : status
1060
+
1061
+ const methodSupportsRequestBody = (method: OpenAPISpecMethodName): boolean =>
1062
+ method !== "get" && method !== "head" && method !== "options" && method !== "trace"
1063
+
1064
+ const warnForOperation = (
1065
+ emitWarning: WarningEmitter,
1066
+ operation: ParsedOperation.ParsedOperation,
1067
+ warning: {
1068
+ readonly code: OpenApiGeneratorWarningCode
1069
+ readonly message: string
1070
+ }
1071
+ ): void => {
1072
+ emitWarning({
1073
+ ...warning,
1074
+ path: operation.path,
1075
+ method: operation.method,
1076
+ operationId: operation.operationId
1077
+ })
1078
+ }
270
1079
 
271
1080
  function getDialect(spec: OpenAPISpec): "openapi-3.0" | "openapi-3.1" {
272
1081
  return spec.openapi.trim().startsWith("3.0") ? "openapi-3.0" : "openapi-3.1"
273
1082
  }
274
1083
 
1084
+ /**
1085
+ * Layer providing an OpenAPI generator for Schema-backed HTTP client and HttpApi output.
1086
+ *
1087
+ * @category layers
1088
+ * @since 4.0.0
1089
+ */
275
1090
  export const layerTransformerSchema: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
276
1091
 
1092
+ /**
1093
+ * Layer providing an OpenAPI generator for type-only HTTP client output.
1094
+ *
1095
+ * @category layers
1096
+ * @since 4.0.0
1097
+ */
277
1098
  export const layerTransformerTs: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
278
1099
 
279
1100
  const isSwaggerSpec = (spec: OpenAPISpec) => "swagger" in spec