@effect/openapi-generator 4.0.0-beta.9 → 4.0.0-beta.90

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 +393 -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 +763 -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 +88 -11
  20. package/dist/OpenApiTransformer.js.map +1 -1
  21. package/dist/ParsedOperation.d.ts +184 -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 +552 -0
  35. package/src/JsonSchemaGenerator.ts +272 -47
  36. package/src/OpenApiGenerator.ts +1057 -165
  37. package/src/OpenApiPatch.ts +56 -31
  38. package/src/OpenApiTransformer.ts +92 -15
  39. package/src/ParsedOperation.ts +235 -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,1028 @@ 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
74
163
  }
164
+ )
165
+ : generator.generate(
166
+ source,
167
+ spec.components?.schemas ?? {},
168
+ options.format === "httpclient-type-only",
169
+ {
170
+ onEnter: options.onEnter
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
- }
244
+ for (const [path, methods] of Object.entries(spec.paths)) {
245
+ for (const method of methodNames) {
246
+ const operation = methods[method]
110
247
 
111
- if (parameter.in === "path") {
112
- continue
113
- }
248
+ if (Predicate.isUndefined(operation)) {
249
+ continue
250
+ }
114
251
 
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
- }
252
+ const id = operation.operationId
253
+ ? Utils.camelize(operation.operationId)
254
+ : `${method.toUpperCase()}${path}`
137
255
 
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
- }
256
+ const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary)
146
257
 
147
- op.params = generator.addSchema(
148
- `${schemaId}Params`,
149
- schema
150
- )
258
+ const { pathIds, pathTemplate } = processPath(path)
151
259
 
152
- op.paramsOptional = !schema.required || schema.required.length === 0
153
- }
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
+ }
154
288
 
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
- }
289
+ const schemaId = Utils.identifier(operation.operationId ?? path)
161
290
 
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
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
311
+ }
312
+ case "query": {
313
+ op.parameters.query.push(parsedParameter)
314
+ break
315
+ }
316
+ case "header": {
317
+ op.parameters.header.push(parsedParameter)
318
+ break
168
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
+ })
350
+
351
+ if (isHttpApi && hasUnsupportedSuccessfulSseResponse(resolvedResponses, hasExplicitSuccessResponse)) {
352
+ warnForOperation(emitWarning, op, {
353
+ code: "sse-operation-skipped",
354
+ message:
355
+ "Operation was skipped because a successful SSE response is missing x-effect-stream metadata required for HttpApi generation."
356
+ })
357
+ continue
358
+ }
359
+
360
+ const validParameters = parameters.filter((parameter) => parameter.in !== "path" && parameter.in !== "cookie")
361
+
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
+ }
178
416
 
179
- if (Predicate.isNotUndefined(response.content?.["application/json"]?.schema)) {
180
- const schemaName = generator.addSchema(
181
- `${schemaId}${status}`,
182
- response.content["application/json"].schema
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
+ }
426
+
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
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
183
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
+ }
184
468
 
185
- if (status === "default") {
186
- defaultSchema = schemaName
187
- continue
188
- }
469
+ let defaultSchema: string | undefined
470
+ for (const [status, response] of resolvedResponses) {
471
+ if (!Predicate.isObject(response)) {
472
+ continue
473
+ }
189
474
 
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
- }
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)) {
515
+ continue
200
516
  }
201
517
 
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(
518
+ const statusMajorNumber = Number(parsedStatus[0])
519
+ const streamEncoding = !Number.isNaN(statusMajorNumber) && statusMajorNumber < 4
520
+ ? getEffectStreamEncoding(mediaType)
521
+ : undefined
522
+ if (streamEncoding === "uint8array") {
523
+ representable.push({
524
+ contentType,
525
+ encoding: "binary",
526
+ effectStream: "uint8array"
527
+ })
528
+ continue
529
+ }
530
+ if (streamEncoding === "sse") {
531
+ const errorSchema = getEffectStreamErrorSchema(mediaType)
532
+ if (Predicate.isUndefined(mediaType.schema) || Predicate.isUndefined(errorSchema)) {
533
+ continue
534
+ }
535
+ representable.push({
536
+ contentType,
537
+ encoding: "text",
538
+ effectStream: "sse",
539
+ schema: addSchema(
210
540
  `${schemaId}${status}Sse`,
211
- response.content["text/event-stream"].schema
541
+ mediaType.schema as JsonSchema.JsonSchema,
542
+ op
543
+ ),
544
+ errorSchema: addSchema(
545
+ `${schemaId}${status}SseError`,
546
+ errorSchema,
547
+ op
212
548
  )
213
- }
549
+ })
550
+ continue
214
551
  }
215
552
 
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
- }
553
+ if (Predicate.isUndefined(mediaType.schema)) {
554
+ continue
222
555
  }
223
-
224
- if (Predicate.isUndefined(response.content)) {
225
- if (status !== "default") {
226
- op.voidSchemas.add(status.toLowerCase())
227
- }
556
+ const encoding = getResponseMediaTypeEncoding(contentType)
557
+ if (encoding === undefined) {
558
+ continue
228
559
  }
560
+ const schemaName = addSchema(
561
+ `${schemaId}${status}${mediaTypeToSuffix(contentType)}`,
562
+ mediaType.schema as JsonSchema.JsonSchema,
563
+ op
564
+ )
565
+ representable.push({
566
+ contentType,
567
+ encoding,
568
+ schema: schemaName
569
+ })
570
+ }
571
+ }
572
+
573
+ const isEmptyResponse = Predicate.isUndefined(content) || Object.keys(content).length === 0
574
+ const parsedResponse = {
575
+ status: parsedStatus,
576
+ description: Utils.nonEmptyString(response.description),
577
+ contentTypes: Predicate.isNotUndefined(content) ? Object.keys(content) : [],
578
+ hasHeaders: Predicate.isNotUndefined(response.headers),
579
+ isEmpty: isEmptyResponse,
580
+ representable
581
+ }
582
+ op.responses.push(parsedResponse)
583
+ if (status === "default") {
584
+ op.defaultResponse = parsedResponse
585
+ }
586
+
587
+ if (Predicate.isNotUndefined(jsonSchemaName)) {
588
+ const schemaName = jsonSchemaName
589
+
590
+ if (status === "default" && !isHttpApi) {
591
+ defaultSchema = schemaName
592
+ continue
229
593
  }
230
594
 
231
- if (op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
232
- op.successSchemas.set("2xx", defaultSchema)
595
+ const statusLower = parsedStatus.toLowerCase()
596
+ const statusMajorNumber = Number(parsedStatus[0])
597
+ if (Number.isNaN(statusMajorNumber)) {
598
+ continue
233
599
  }
600
+ if (statusMajorNumber < 4) {
601
+ op.successSchemas.set(statusLower, schemaName)
602
+ } else {
603
+ op.errorSchemas.set(statusLower, schemaName)
604
+ }
605
+ }
234
606
 
235
- operations.push(op)
607
+ const sseResponseSchema = content?.["text/event-stream"]?.schema
608
+ if (!isHttpApi && Predicate.isUndefined(op.sseSchema) && Predicate.isNotUndefined(sseResponseSchema)) {
609
+ const statusMajorNumber = Number(parsedStatus[0])
610
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
611
+ op.sseSchema = addSchema(`${schemaId}${status}Sse`, sseResponseSchema, op)
612
+ }
613
+ }
614
+
615
+ if (Predicate.isNotUndefined(content?.["application/octet-stream"])) {
616
+ const statusMajorNumber = Number(parsedStatus[0])
617
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
618
+ op.binaryResponse = true
619
+ }
620
+ }
621
+
622
+ if (isEmptyResponse) {
623
+ if (parsedStatus !== "default") {
624
+ op.voidSchemas.add(parsedStatus.toLowerCase())
625
+ }
236
626
  }
237
627
  }
238
628
 
239
- for (const [path, methods] of Object.entries(spec.paths)) {
240
- handlePath(path, methods)
629
+ if (!isHttpApi && op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
630
+ op.successSchemas.set("2xx", defaultSchema)
631
+ warnForOperation(emitWarning, op, {
632
+ code: "default-response-remapped",
633
+ message: "Default response was remapped to 2xx for the current HttpClient outputs."
634
+ })
241
635
  }
242
636
 
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
- })
637
+ operations.push(op)
638
+ }
639
+ }
249
640
 
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
- )
641
+ return {
642
+ metadata: {
643
+ title: spec.info.title,
644
+ version: spec.info.version,
645
+ summary: Utils.nonEmptyString(spec.info.summary),
646
+ description: Utils.nonEmptyString(spec.info.description),
647
+ license: spec.info.license,
648
+ servers: spec.servers
257
649
  },
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
- )
650
+ tags: (spec.tags ?? []).map((tag) => ({
651
+ name: tag.name,
652
+ description: Utils.nonEmptyString(tag.description),
653
+ externalDocs: tag.externalDocs
654
+ })),
655
+ securitySchemes,
656
+ operations
657
+ }
658
+ }
659
+
660
+ interface OpenApiParameter {
661
+ readonly name: string
662
+ readonly in: "path" | "query" | "header" | "cookie"
663
+ readonly required: boolean
664
+ readonly schema: {}
665
+ readonly description?: string | undefined
666
+ }
667
+
668
+ const isOpenApiParameter = (parameter: unknown): parameter is OpenApiParameter => {
669
+ if (!Predicate.isObject(parameter)) {
670
+ return false
671
+ }
672
+ return (
673
+ typeof parameter.name === "string" &&
674
+ (parameter.in === "path" || parameter.in === "query" || parameter.in === "header" || parameter.in === "cookie")
266
675
  )
676
+ }
267
677
 
268
- return { generate } as const
269
- })
678
+ const resolveOperationParameters = (
679
+ pathParameters: ReadonlyArray<unknown> | undefined,
680
+ operationParameters: ReadonlyArray<unknown> | undefined,
681
+ resolveRef: (ref: string) => unknown
682
+ ): Array<OpenApiParameter> => {
683
+ const resolved = new Map<string, OpenApiParameter>()
684
+ const add = (parameter: unknown): void => {
685
+ const current = resolveReference(parameter, resolveRef)
686
+ if (!isOpenApiParameter(current)) {
687
+ return
688
+ }
689
+
690
+ const key = `${current.in}:${current.name}`
691
+ if (resolved.has(key)) {
692
+ resolved.delete(key)
693
+ }
694
+ resolved.set(key, current)
695
+ }
696
+
697
+ for (const parameter of pathParameters ?? []) {
698
+ add(parameter)
699
+ }
700
+
701
+ for (const parameter of operationParameters ?? []) {
702
+ add(parameter)
703
+ }
704
+
705
+ return [...resolved.values()]
706
+ }
707
+
708
+ const buildParameterSchema = <
709
+ Parameter extends {
710
+ readonly name: string
711
+ readonly required: boolean
712
+ readonly schema: {}
713
+ readonly in?: "path" | "query" | "header" | "cookie" | undefined
714
+ }
715
+ >(
716
+ parameters: ReadonlyArray<Parameter>,
717
+ onAdded?: ((parameter: Parameter, added: Array<string>) => void) | undefined
718
+ ): {
719
+ readonly schema: JsonSchema.JsonSchema
720
+ readonly optional: boolean
721
+ } | undefined => {
722
+ if (parameters.length === 0) {
723
+ return
724
+ }
725
+
726
+ const schema = {
727
+ type: "object" as JsonSchema.Type,
728
+ properties: {} as Record<string, JsonSchema.JsonSchema>,
729
+ required: [] as Array<string>,
730
+ additionalProperties: false
731
+ }
732
+
733
+ for (const parameter of parameters) {
734
+ const paramSchema = parameter.schema as any
735
+ const added: Array<string> = []
736
+ if (
737
+ Predicate.isObject(paramSchema) &&
738
+ "properties" in paramSchema &&
739
+ Predicate.isObject(paramSchema.properties)
740
+ ) {
741
+ const required = "required" in paramSchema
742
+ ? paramSchema.required as Array<string>
743
+ : []
744
+
745
+ for (const [name, propertySchema] of Object.entries(paramSchema.properties)) {
746
+ const adjustedName = `${parameter.name}[${name}]`
747
+ schema.properties[adjustedName] = propertySchema as JsonSchema.JsonSchema
748
+ if (required.includes(name)) {
749
+ schema.required.push(adjustedName)
750
+ }
751
+ added.push(adjustedName)
752
+ }
753
+ } else {
754
+ schema.properties[parameter.name] = parameter.schema as JsonSchema.JsonSchema
755
+ if (parameter.required) {
756
+ schema.required.push(parameter.name)
757
+ }
758
+ added.push(parameter.name)
759
+ }
760
+
761
+ onAdded?.(parameter, added)
762
+ }
763
+
764
+ return {
765
+ schema,
766
+ optional: schema.required.length === 0
767
+ }
768
+ }
769
+
770
+ const mediaTypeToSuffix = (contentType: string): string => {
771
+ const normalized = contentType.toLowerCase()
772
+ switch (normalized) {
773
+ case "application/json":
774
+ return "Json"
775
+ case "multipart/form-data":
776
+ return "FormData"
777
+ case "application/x-www-form-urlencoded":
778
+ return "FormUrlEncoded"
779
+ case "text/plain":
780
+ return "Text"
781
+ case "application/octet-stream":
782
+ return "Binary"
783
+ }
784
+ const suffix = Utils.identifier(contentType)
785
+ return suffix.length > 0 ? suffix : "Body"
786
+ }
787
+
788
+ const makeHttpApiMultipartSchemaRefs = (definitions: JsonSchema.Definitions): HttpApiMultipartSchemaRefs => {
789
+ const names = new Set(Object.keys(definitions))
790
+ const allocate = (base: string): string => {
791
+ let candidate = base
792
+ let index = 2
793
+ while (names.has(candidate)) {
794
+ candidate = `${base}${index}`
795
+ index += 1
796
+ }
797
+ names.add(candidate)
798
+ return candidate
799
+ }
800
+ return {
801
+ singleFile: allocate("__HttpApiMultipartSingleFile"),
802
+ files: allocate("__HttpApiMultipartFiles")
803
+ }
804
+ }
805
+
806
+ const toDefinitionRef = (name: string): string => `#/$defs/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`
807
+
808
+ const withHttpApiMultipartSchemas = (
809
+ definitions: JsonSchema.Definitions,
810
+ multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined
811
+ ): JsonSchema.Definitions => {
812
+ if (multipartSchemaRefs === undefined) {
813
+ return definitions
814
+ }
815
+ return {
816
+ ...definitions,
817
+ [multipartSchemaRefs.singleFile]: {
818
+ type: "string",
819
+ format: "binary"
820
+ },
821
+ [multipartSchemaRefs.files]: {
822
+ type: "array",
823
+ items: {
824
+ $ref: toDefinitionRef(multipartSchemaRefs.singleFile)
825
+ }
826
+ }
827
+ }
828
+ }
829
+
830
+ const transformMultipartSchema = (
831
+ schema: JsonSchema.JsonSchema,
832
+ multipartSchemaRefs: HttpApiMultipartSchemaRefs | undefined,
833
+ resolveRef: (ref: string) => unknown
834
+ ): JsonSchema.JsonSchema => {
835
+ if (multipartSchemaRefs === undefined) {
836
+ return schema
837
+ }
838
+
839
+ const singleFileRef = toDefinitionRef(multipartSchemaRefs.singleFile)
840
+ const filesRef = toDefinitionRef(multipartSchemaRefs.files)
841
+ const cache = new Map<string, unknown>()
842
+ const stack = new Set<string>()
843
+
844
+ const visit = (value: unknown): unknown => {
845
+ if (Array.isArray(value)) {
846
+ return value.map(visit)
847
+ }
848
+ if (!Predicate.isObject(value)) {
849
+ return value
850
+ }
851
+
852
+ if (typeof value.$ref === "string" && value.$ref.startsWith("#/components/schemas/")) {
853
+ const cached = cache.get(value.$ref)
854
+ if (cached !== undefined) {
855
+ return cached
856
+ }
857
+ if (stack.has(value.$ref)) {
858
+ return value
859
+ }
860
+ stack.add(value.$ref)
861
+ const transformed = visit(resolveSchemaReference(value.$ref, resolveRef))
862
+ stack.delete(value.$ref)
863
+ cache.set(value.$ref, transformed)
864
+ return transformed
865
+ }
866
+
867
+ if (isMultipartBinaryFile(value)) {
868
+ return { $ref: singleFileRef }
869
+ }
870
+
871
+ const out: Record<string, unknown> = {}
872
+ for (const [key, current] of Object.entries(value)) {
873
+ out[key] = visit(current)
874
+ }
875
+
876
+ if (isMultipartBinaryFiles(out, singleFileRef)) {
877
+ return { $ref: filesRef }
878
+ }
879
+
880
+ return out
881
+ }
882
+
883
+ return visit(schema) as JsonSchema.JsonSchema
884
+ }
885
+
886
+ const resolveSchemaReference = (ref: string, resolveRef: (ref: string) => unknown): unknown => {
887
+ let current: unknown = { $ref: ref }
888
+ const seen = new Set<string>()
889
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
890
+ if (seen.has(current.$ref)) {
891
+ return current
892
+ }
893
+ seen.add(current.$ref)
894
+ current = resolveRef(current.$ref)
895
+ }
896
+ return current
897
+ }
898
+
899
+ const isMultipartBinaryFile = (value: unknown): value is JsonSchema.JsonSchema =>
900
+ Predicate.isObject(value) &&
901
+ value.type === "string" &&
902
+ (
903
+ (typeof value.format === "string" && value.format.toLowerCase() === "binary") ||
904
+ (typeof value.contentEncoding === "string" && value.contentEncoding.toLowerCase() === "binary")
905
+ )
906
+
907
+ const isMultipartBinaryFiles = (value: Record<string, unknown>, singleFileRef: string): boolean => {
908
+ if (value.type !== "array") {
909
+ return false
910
+ }
911
+ const items = value.items
912
+ return isMultipartBinaryFile(items) || (Predicate.isObject(items) && items.$ref === singleFileRef)
913
+ }
914
+
915
+ const isJsonMediaType = (contentType: string): boolean =>
916
+ contentType === "application/json" ||
917
+ (contentType.startsWith("application/") && contentType.endsWith("+json"))
918
+
919
+ const isTextMediaType = (contentType: string): boolean => contentType.startsWith("text/")
920
+
921
+ const isBinaryMediaType = (contentType: string): boolean =>
922
+ contentType === "application/octet-stream" ||
923
+ (contentType.startsWith("application/") && (contentType.includes("binary") || contentType.endsWith("+octet-stream")))
924
+
925
+ const getRequestMediaTypeEncoding = (
926
+ contentType: string
927
+ ): ParsedOperation.ParsedOperationMediaTypeEncoding | undefined => {
928
+ const normalized = contentType.toLowerCase()
929
+ if (isJsonMediaType(normalized)) {
930
+ return "json"
931
+ }
932
+ if (normalized === "multipart/form-data") {
933
+ return "multipart"
934
+ }
935
+ if (normalized === "application/x-www-form-urlencoded") {
936
+ return "form-url-encoded"
937
+ }
938
+ if (isTextMediaType(normalized)) {
939
+ return "text"
940
+ }
941
+ if (isBinaryMediaType(normalized)) {
942
+ return "binary"
943
+ }
944
+ return
945
+ }
946
+
947
+ const getResponseMediaTypeEncoding = (
948
+ contentType: string
949
+ ): ParsedOperation.ParsedOperationMediaTypeEncoding | undefined => {
950
+ const normalized = contentType.toLowerCase()
951
+ if (isJsonMediaType(normalized)) {
952
+ return "json"
953
+ }
954
+ if (normalized === "application/x-www-form-urlencoded") {
955
+ return "form-url-encoded"
956
+ }
957
+ if (isTextMediaType(normalized)) {
958
+ return "text"
959
+ }
960
+ if (isBinaryMediaType(normalized)) {
961
+ return "binary"
962
+ }
963
+ return
964
+ }
965
+
966
+ const getEffectStreamEncoding = (mediaType: object): "uint8array" | "sse" | undefined => {
967
+ const stream = (mediaType as Record<string, unknown>)["x-effect-stream"]
968
+ if (!Predicate.isObject(stream)) {
969
+ return
970
+ }
971
+ return stream.encoding === "uint8array" || stream.encoding === "sse" ? stream.encoding : undefined
972
+ }
973
+
974
+ const getEffectStreamErrorSchema = (mediaType: object): JsonSchema.JsonSchema | undefined => {
975
+ const stream = (mediaType as Record<string, unknown>)["x-effect-stream"]
976
+ if (!Predicate.isObject(stream) || !Predicate.isObject(stream.errorSchema)) {
977
+ return
978
+ }
979
+ return stream.errorSchema as JsonSchema.JsonSchema
980
+ }
981
+
982
+ const resolveReference = (input: unknown, resolveRef: (ref: string) => unknown): any => {
983
+ let current = input
984
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
985
+ current = resolveRef(current.$ref)
986
+ }
987
+ return current
988
+ }
989
+
990
+ const cloneSecurityRequirements = (
991
+ security: ReadonlyArray<Record<string, ReadonlyArray<string>>>
992
+ ): Array<ParsedOperation.ParsedOperationSecurityRequirement> =>
993
+ security.map((requirement) =>
994
+ Object.fromEntries(
995
+ Object.entries(requirement).map(([name, scopes]) => [name, [...scopes]])
996
+ )
997
+ )
998
+
999
+ const parseSecuritySchemes = (
1000
+ spec: OpenAPISpec,
1001
+ resolveRef: (ref: string) => unknown
1002
+ ): Array<ParsedOperation.ParsedOpenApiSecurityScheme> => {
1003
+ const securitySchemes = spec.components?.securitySchemes ?? {}
1004
+ const parsed: Array<ParsedOperation.ParsedOpenApiSecurityScheme> = []
1005
+
1006
+ for (const [name, value] of Object.entries(securitySchemes)) {
1007
+ const scheme = resolveReference(value, resolveRef) as OpenAPISecurityScheme | undefined
1008
+ if (!Predicate.isObject(scheme)) {
1009
+ continue
1010
+ }
1011
+
1012
+ if (scheme.type === "http" && typeof scheme.scheme === "string") {
1013
+ const normalizedScheme = scheme.scheme.toLowerCase()
1014
+ if (normalizedScheme === "basic") {
1015
+ parsed.push({
1016
+ name,
1017
+ type: "basic",
1018
+ description: Utils.nonEmptyString(scheme.description),
1019
+ bearerFormat: undefined,
1020
+ scheme: undefined,
1021
+ key: undefined,
1022
+ in: undefined
1023
+ })
1024
+ } else if (normalizedScheme === "bearer") {
1025
+ parsed.push({
1026
+ name,
1027
+ type: "bearer",
1028
+ description: Utils.nonEmptyString(scheme.description),
1029
+ bearerFormat: Utils.nonEmptyString(scheme.bearerFormat),
1030
+ scheme: undefined,
1031
+ key: undefined,
1032
+ in: undefined
1033
+ })
1034
+ } else {
1035
+ parsed.push({
1036
+ name,
1037
+ type: "http",
1038
+ description: Utils.nonEmptyString(scheme.description),
1039
+ bearerFormat: Utils.nonEmptyString(scheme.bearerFormat),
1040
+ scheme: scheme.scheme,
1041
+ key: undefined,
1042
+ in: undefined
1043
+ })
1044
+ }
1045
+ continue
1046
+ }
1047
+
1048
+ if (
1049
+ scheme.type === "apiKey" &&
1050
+ (scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie") &&
1051
+ typeof scheme.name === "string"
1052
+ ) {
1053
+ parsed.push({
1054
+ name,
1055
+ type: "apiKey",
1056
+ description: Utils.nonEmptyString(scheme.description),
1057
+ bearerFormat: undefined,
1058
+ scheme: undefined,
1059
+ key: scheme.name,
1060
+ in: scheme.in
1061
+ })
1062
+ }
1063
+ }
1064
+
1065
+ return parsed
1066
+ }
1067
+
1068
+ const warnForAndSecurityRequirements = (
1069
+ emitWarning: WarningEmitter,
1070
+ operation: ParsedOperation.ParsedOperation
1071
+ ): void => {
1072
+ if (operation.effectiveSecurity.some((requirement) => Object.keys(requirement).length === 0)) {
1073
+ return
1074
+ }
1075
+ for (const requirement of operation.effectiveSecurity) {
1076
+ const schemes = Object.keys(requirement)
1077
+ if (schemes.length <= 1) {
1078
+ continue
1079
+ }
1080
+ warnForOperation(emitWarning, operation, {
1081
+ code: "security-and-downgraded",
1082
+ message: `Security requirement requiring all of [${
1083
+ schemes.join(", ")
1084
+ }] was downgraded to a placeholder middleware.`
1085
+ })
1086
+ }
1087
+ }
1088
+
1089
+ const hasUnsupportedSuccessfulSseResponse = (
1090
+ responses: ReadonlyArray<readonly [string, unknown]>,
1091
+ hasExplicitSuccessResponse: boolean
1092
+ ): boolean => {
1093
+ for (const [status, response] of responses) {
1094
+ if (!Predicate.isObject(response)) {
1095
+ continue
1096
+ }
1097
+ const remappedStatus = remapDefaultResponseStatusForHttpApi(status, hasExplicitSuccessResponse)
1098
+ const statusCode = Number(remappedStatus)
1099
+ if (Number.isNaN(statusCode) || statusCode >= 400) {
1100
+ continue
1101
+ }
1102
+
1103
+ const content = Predicate.isObject(response.content)
1104
+ ? response.content as Record<string, any>
1105
+ : undefined
1106
+ if (Predicate.isUndefined(content)) {
1107
+ continue
1108
+ }
1109
+
1110
+ for (const [contentType, mediaType] of Object.entries(content)) {
1111
+ if (!Predicate.isObject(mediaType)) {
1112
+ continue
1113
+ }
1114
+ const streamEncoding = getEffectStreamEncoding(mediaType)
1115
+ if (streamEncoding === "sse") {
1116
+ if (Predicate.isUndefined(mediaType.schema) || Predicate.isUndefined(getEffectStreamErrorSchema(mediaType))) {
1117
+ return true
1118
+ }
1119
+ continue
1120
+ }
1121
+ if (contentType === "text/event-stream") {
1122
+ return true
1123
+ }
1124
+ }
1125
+ }
1126
+ return false
1127
+ }
1128
+
1129
+ const remapDefaultResponseStatusForHttpApi = (status: string, hasExplicitSuccessResponse: boolean): string =>
1130
+ status === "default" ? (hasExplicitSuccessResponse ? "500" : "200") : status
1131
+
1132
+ const methodSupportsRequestBody = (method: OpenAPISpecMethodName): boolean =>
1133
+ method !== "get" && method !== "head" && method !== "options" && method !== "trace"
1134
+
1135
+ const warnForOperation = (
1136
+ emitWarning: WarningEmitter,
1137
+ operation: ParsedOperation.ParsedOperation,
1138
+ warning: {
1139
+ readonly code: OpenApiGeneratorWarningCode
1140
+ readonly message: string
1141
+ }
1142
+ ): void => {
1143
+ emitWarning({
1144
+ ...warning,
1145
+ path: operation.path,
1146
+ method: operation.method,
1147
+ operationId: operation.operationId
1148
+ })
1149
+ }
270
1150
 
271
1151
  function getDialect(spec: OpenAPISpec): "openapi-3.0" | "openapi-3.1" {
272
1152
  return spec.openapi.trim().startsWith("3.0") ? "openapi-3.0" : "openapi-3.1"
273
1153
  }
274
1154
 
1155
+ /**
1156
+ * Layer providing an OpenAPI generator for Schema-backed HTTP client and HttpApi output.
1157
+ *
1158
+ * @category layers
1159
+ * @since 4.0.0
1160
+ */
275
1161
  export const layerTransformerSchema: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
276
1162
 
1163
+ /**
1164
+ * Layer providing an OpenAPI generator for type-only HTTP client output.
1165
+ *
1166
+ * @category layers
1167
+ * @since 4.0.0
1168
+ */
277
1169
  export const layerTransformerTs: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
278
1170
 
279
1171
  const isSwaggerSpec = (spec: OpenAPISpec) => "swagger" in spec