@effect/openapi-generator 4.0.0-beta.6 → 4.0.0-beta.62

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.
@@ -1,19 +1,21 @@
1
+ import * as Context from "effect/Context";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
4
  import * as Predicate from "effect/Predicate";
4
- import * as ServiceMap from "effect/ServiceMap";
5
5
  import * as String from "effect/String";
6
6
  import SwaggerToOpenApi from "swagger2openapi";
7
+ import * as HttpApiTransformer from "./HttpApiTransformer.js";
7
8
  import * as JsonSchemaGenerator from "./JsonSchemaGenerator.js";
8
9
  import * as OpenApiTransformer from "./OpenApiTransformer.js";
9
10
  import * as ParsedOperation from "./ParsedOperation.js";
10
11
  import * as Utils from "./Utils.js";
11
- export class OpenApiGenerator extends /*#__PURE__*/ServiceMap.Service()("OpenApiGenerator") {}
12
+ export class OpenApiGenerator extends /*#__PURE__*/Context.Service()("OpenApiGenerator") {}
12
13
  const methodNames = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
13
14
  export const make = /*#__PURE__*/Effect.gen(function* () {
14
15
  const generate = Effect.fn(function* (spec, options) {
15
16
  const generator = JsonSchemaGenerator.make();
16
17
  const openApiTransformer = yield* OpenApiTransformer.OpenApiTransformer;
18
+ const emitWarning = makeWarningEmitter(options);
17
19
  // If we receive a Swagger 2.0 spec, convert it to an OpenApi 3.0 spec
18
20
  if (isSwaggerSpec(spec)) {
19
21
  spec = yield* convertSwaggerSpec(spec);
@@ -26,150 +28,686 @@ export const make = /*#__PURE__*/Effect.gen(function* () {
26
28
  }
27
29
  return current;
28
30
  }
29
- const operations = [];
30
- function handlePath(path, methods) {
31
- for (const method of methodNames) {
32
- const operation = methods[method];
33
- if (Predicate.isUndefined(operation)) {
34
- continue;
35
- }
36
- const id = operation.operationId ? Utils.camelize(operation.operationId) : `${method.toUpperCase()}${path}`;
37
- const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary);
38
- const {
39
- pathIds,
40
- pathTemplate
41
- } = processPath(path);
42
- const op = ParsedOperation.makeDeepMutable({
43
- id,
44
- method,
45
- description,
46
- pathIds,
47
- pathTemplate
31
+ const multipartSchemaRefs = options.format === "httpapi" ? makeHttpApiMultipartSchemaRefs(spec.components?.schemas ?? {}) : undefined;
32
+ const parsed = parseOpenApi(spec, generator, resolveRef, options.format, emitWarning, multipartSchemaRefs);
33
+ // TODO: make a CLI option ?
34
+ const importName = "Schema";
35
+ const source = getDialect(spec);
36
+ const generation = options.format === "httpapi" ? generator.generateHttpApi(source, withHttpApiMultipartSchemas(spec.components?.schemas ?? {}, multipartSchemaRefs), {
37
+ onEnter: options.onEnter,
38
+ multipartSchemaRefs
39
+ }) : generator.generate(source, spec.components?.schemas ?? {}, options.format === "httpclient-type-only", {
40
+ onEnter: options.onEnter
41
+ });
42
+ if (options.format === "httpapi") {
43
+ const needsMultipartImport = generation.includes("Multipart.");
44
+ return String.stripMargin(`|${HttpApiTransformer.imports(importName, {
45
+ multipart: needsMultipartImport
46
+ })}
47
+ |${generation}
48
+ |${HttpApiTransformer.toImplementation(importName, options.name, parsed)}`);
49
+ }
50
+ return String.stripMargin(`|${openApiTransformer.imports(importName, parsed)}
51
+ |${generation}
52
+ |${openApiTransformer.toImplementation(importName, options.name, parsed)}
53
+ |
54
+ |${openApiTransformer.toTypes(importName, options.name, parsed)}`);
55
+ }, (effect, _, options) => Effect.provideServiceEffect(effect, OpenApiTransformer.OpenApiTransformer, options.format === "httpclient-type-only" ? Effect.sync(OpenApiTransformer.makeTransformerTs) : Effect.sync(OpenApiTransformer.makeTransformerSchema)));
56
+ return {
57
+ generate
58
+ };
59
+ });
60
+ const makeWarningEmitter = options => warning => {
61
+ options.onWarning?.(warning);
62
+ };
63
+ const parseOpenApi = (spec, generator, resolveRef, format, emitWarning, multipartSchemaRefs) => {
64
+ const operations = [];
65
+ const reservedSchemaNames = new Set(Object.keys(spec.components?.schemas ?? {}));
66
+ const isHttpApi = format === "httpapi";
67
+ const securitySchemes = isHttpApi ? parseSecuritySchemes(spec, resolveRef) : [];
68
+ const addSchema = (baseName, schema, operation) => {
69
+ let candidate = baseName;
70
+ let index = 2;
71
+ while (reservedSchemaNames.has(candidate)) {
72
+ candidate = `${baseName}${index}`;
73
+ index += 1;
74
+ }
75
+ if (candidate !== baseName) {
76
+ warnForOperation(emitWarning, operation, {
77
+ code: "naming-collision",
78
+ message: `Schema name "${baseName}" collided with an existing name and was renamed to "${candidate}".`
79
+ });
80
+ }
81
+ reservedSchemaNames.add(candidate);
82
+ return generator.addSchema(candidate, schema);
83
+ };
84
+ for (const [path, methods] of Object.entries(spec.paths)) {
85
+ for (const method of methodNames) {
86
+ const operation = methods[method];
87
+ if (Predicate.isUndefined(operation)) {
88
+ continue;
89
+ }
90
+ const id = operation.operationId ? Utils.camelize(operation.operationId) : `${method.toUpperCase()}${path}`;
91
+ const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary);
92
+ const {
93
+ pathIds,
94
+ pathTemplate
95
+ } = processPath(path);
96
+ const op = ParsedOperation.makeDeepMutable({
97
+ id,
98
+ method,
99
+ description,
100
+ pathIds,
101
+ pathTemplate
102
+ });
103
+ op.path = path;
104
+ op.operationId = Utils.nonEmptyString(operation.operationId);
105
+ op.tags = [...(operation.tags ?? [])];
106
+ if (isHttpApi && op.tags.length > 1) {
107
+ warnForOperation(emitWarning, op, {
108
+ code: "additional-tags-dropped",
109
+ message: `Additional tags (${op.tags.slice(1).join(", ")}) were dropped. Only the first tag ("${op.tags[0]}") is used for grouping.`
48
110
  });
49
- const schemaId = Utils.identifier(operation.operationId ?? path);
50
- const validParameters = operation.parameters?.filter(param => {
51
- return param.in !== "path" && param.in !== "cookie";
52
- }) ?? [];
53
- if (validParameters.length > 0) {
54
- const schema = {
55
- type: "object",
56
- properties: {},
57
- required: [],
58
- additionalProperties: false
59
- };
60
- for (let parameter of validParameters) {
61
- if ("$ref" in parameter) {
62
- parameter = resolveRef(parameter.$ref);
111
+ }
112
+ op.metadata = {
113
+ summary: Utils.nonEmptyString(operation.summary),
114
+ description: Utils.nonEmptyString(operation.description),
115
+ deprecated: operation.deprecated === true,
116
+ externalDocs: operation.externalDocs
117
+ };
118
+ op.effectiveSecurity = cloneSecurityRequirements(operation.security ?? spec.security ?? []);
119
+ if (isHttpApi) {
120
+ warnForAndSecurityRequirements(emitWarning, op);
121
+ }
122
+ const schemaId = Utils.identifier(operation.operationId ?? path);
123
+ const pathParameters = Predicate.isObject(methods) && Array.isArray(methods.parameters) ? methods.parameters : undefined;
124
+ const parameters = resolveOperationParameters(pathParameters, Array.isArray(operation.parameters) ? operation.parameters : undefined, resolveRef);
125
+ for (const parameter of parameters) {
126
+ const parsedParameter = {
127
+ name: parameter.name,
128
+ in: parameter.in,
129
+ required: parameter.required === true,
130
+ description: Utils.nonEmptyString(parameter.description),
131
+ schema: parameter.schema
132
+ };
133
+ switch (parameter.in) {
134
+ case "path":
135
+ {
136
+ op.parameters.path.push(parsedParameter);
137
+ break;
63
138
  }
64
- if (parameter.in === "path") {
139
+ case "query":
140
+ {
141
+ op.parameters.query.push(parsedParameter);
142
+ break;
143
+ }
144
+ case "header":
145
+ {
146
+ op.parameters.header.push(parsedParameter);
147
+ break;
148
+ }
149
+ case "cookie":
150
+ {
151
+ op.parameters.cookie.push(parsedParameter);
152
+ warnForOperation(emitWarning, op, {
153
+ code: "cookie-parameter-dropped",
154
+ message: `Cookie parameter "${parameter.name}" was dropped because non-security cookie parameters are not supported.`
155
+ });
156
+ break;
157
+ }
158
+ }
159
+ }
160
+ const requestBody = resolveReference(operation.requestBody, resolveRef);
161
+ if (isHttpApi && !methodSupportsRequestBody(op.method) && Predicate.isObject(requestBody)) {
162
+ warnForOperation(emitWarning, op, {
163
+ code: "no-body-method-request-body-skipped",
164
+ message: `Operation was skipped because ${op.method.toUpperCase()} does not support request bodies.`
165
+ });
166
+ continue;
167
+ }
168
+ const resolvedResponses = Object.entries(operation.responses ?? {}).map(([status, response]) => [status, resolveReference(response, resolveRef)]);
169
+ const hasExplicitSuccessResponse = resolvedResponses.some(([status]) => {
170
+ if (!/^\d{3}$/.test(status)) {
171
+ return false;
172
+ }
173
+ return Number(status) < 400;
174
+ });
175
+ if (isHttpApi && hasSuccessfulSseResponse(resolvedResponses, hasExplicitSuccessResponse)) {
176
+ warnForOperation(emitWarning, op, {
177
+ code: "sse-operation-skipped",
178
+ message: "Operation was skipped because successful text/event-stream responses are not supported in HttpApi generation."
179
+ });
180
+ continue;
181
+ }
182
+ const validParameters = parameters.filter(parameter => parameter.in !== "path" && parameter.in !== "cookie");
183
+ const combinedParameterSchema = buildParameterSchema(validParameters, (parameter, added) => {
184
+ if (parameter.in === "query") {
185
+ Utils.spreadElementsInto(added, op.urlParams);
186
+ } else if (parameter.in === "header") {
187
+ Utils.spreadElementsInto(added, op.headers);
188
+ } else if (parameter.in === "cookie") {
189
+ Utils.spreadElementsInto(added, op.cookies);
190
+ }
191
+ });
192
+ if (combinedParameterSchema !== undefined) {
193
+ op.params = addSchema(`${schemaId}Params`, combinedParameterSchema.schema, op);
194
+ op.paramsOptional = combinedParameterSchema.optional;
195
+ }
196
+ if (isHttpApi) {
197
+ const pathParameterSchema = buildParameterSchema(op.parameters.path);
198
+ if (pathParameterSchema !== undefined) {
199
+ op.pathSchema = addSchema(`${schemaId}PathParams`, pathParameterSchema.schema, op);
200
+ }
201
+ const queryParameterSchema = buildParameterSchema(op.parameters.query);
202
+ if (queryParameterSchema !== undefined) {
203
+ op.querySchema = addSchema(`${schemaId}Query`, queryParameterSchema.schema, op);
204
+ op.querySchemaOptional = queryParameterSchema.optional;
205
+ }
206
+ const headerParameterSchema = buildParameterSchema(op.parameters.header);
207
+ if (headerParameterSchema !== undefined) {
208
+ op.headersSchema = addSchema(`${schemaId}Headers`, headerParameterSchema.schema, op);
209
+ op.headersSchemaOptional = headerParameterSchema.optional;
210
+ }
211
+ }
212
+ if (Predicate.isNotUndefined(requestBody) && Predicate.isObject(requestBody)) {
213
+ const content = Predicate.isObject(requestBody.content) ? requestBody.content : {};
214
+ const requestSchemaNames = new Map();
215
+ op.requestBody = {
216
+ required: requestBody.required === true,
217
+ contentTypes: Object.keys(content)
218
+ };
219
+ if (isHttpApi && requestBody.required === false) {
220
+ warnForOperation(emitWarning, op, {
221
+ code: "optional-request-body-approximated",
222
+ message: "Optional request body was approximated by adding a no-content payload alternative."
223
+ });
224
+ }
225
+ if (Predicate.isNotUndefined(content["application/json"]?.schema)) {
226
+ op.payload = addSchema(`${schemaId}RequestJson`, content["application/json"].schema, op);
227
+ requestSchemaNames.set("application/json", op.payload);
228
+ }
229
+ if (Predicate.isNotUndefined(content["multipart/form-data"]?.schema)) {
230
+ op.payload = addSchema(`${schemaId}RequestFormData`, transformMultipartSchema(content["multipart/form-data"].schema, multipartSchemaRefs, resolveRef), op);
231
+ op.payloadFormData = true;
232
+ requestSchemaNames.set("multipart/form-data", op.payload);
233
+ }
234
+ if (Predicate.isNotUndefined(content["application/x-www-form-urlencoded"]?.schema)) {
235
+ op.payload = addSchema(`${schemaId}RequestFormUrlEncoded`, content["application/x-www-form-urlencoded"].schema, op);
236
+ op.payloadFormUrlEncoded = true;
237
+ requestSchemaNames.set("application/x-www-form-urlencoded", op.payload);
238
+ }
239
+ if (isHttpApi) {
240
+ const representableRequestBody = [];
241
+ for (const [contentType, mediaType] of Object.entries(content)) {
242
+ if (!Predicate.isObject(mediaType) || Predicate.isUndefined(mediaType.schema)) {
65
243
  continue;
66
244
  }
67
- const paramSchema = parameter.schema;
68
- const added = [];
69
- if ("properties" in paramSchema && Predicate.isObject(paramSchema.properties)) {
70
- const required = "required" in paramSchema ? paramSchema.required : [];
71
- for (const [name, propSchema] of Object.entries(paramSchema.properties)) {
72
- const adjustedName = `${parameter.name}[${name}]`;
73
- schema.properties[adjustedName] = propSchema;
74
- if (required.includes(name)) {
75
- schema.required.push(adjustedName);
76
- }
77
- added.push(adjustedName);
78
- }
79
- } else {
80
- schema.properties[parameter.name] = parameter.schema;
81
- if (parameter.required) {
82
- schema.required.push(parameter.name);
83
- }
84
- added.push(parameter.name);
245
+ const encoding = getRequestMediaTypeEncoding(contentType);
246
+ if (encoding === undefined) {
247
+ continue;
85
248
  }
86
- if (parameter.in === "query") {
87
- Utils.spreadElementsInto(added, op.urlParams);
88
- } else if (parameter.in === "header") {
89
- Utils.spreadElementsInto(added, op.headers);
90
- } else if (parameter.in === "cookie") {
91
- Utils.spreadElementsInto(added, op.cookies);
249
+ let schemaName = requestSchemaNames.get(contentType);
250
+ if (schemaName === undefined) {
251
+ const schema = encoding === "multipart" ? transformMultipartSchema(mediaType.schema, multipartSchemaRefs, resolveRef) : mediaType.schema;
252
+ schemaName = addSchema(`${schemaId}Request${mediaTypeToSuffix(contentType)}`, schema, op);
253
+ requestSchemaNames.set(contentType, schemaName);
92
254
  }
255
+ representableRequestBody.push({
256
+ contentType,
257
+ encoding,
258
+ schema: schemaName
259
+ });
93
260
  }
94
- op.params = generator.addSchema(`${schemaId}Params`, schema);
95
- op.paramsOptional = !schema.required || schema.required.length === 0;
261
+ op.requestBodyRepresentable = representableRequestBody;
262
+ }
263
+ }
264
+ let defaultSchema;
265
+ for (const [status, response] of resolvedResponses) {
266
+ if (!Predicate.isObject(response)) {
267
+ continue;
96
268
  }
97
- if (Predicate.isNotUndefined(operation.requestBody?.content?.["application/json"]?.schema)) {
98
- op.payload = generator.addSchema(`${schemaId}RequestJson`, operation.requestBody.content["application/json"].schema);
269
+ const parsedStatus = isHttpApi ? remapDefaultResponseStatusForHttpApi(status, hasExplicitSuccessResponse) : status;
270
+ if (isHttpApi && status === "default") {
271
+ warnForOperation(emitWarning, op, {
272
+ code: "default-response-remapped",
273
+ message: `Default response was remapped to status ${parsedStatus} for HttpApi generation.`
274
+ });
99
275
  }
100
- if (Predicate.isNotUndefined(operation.requestBody?.content?.["multipart/form-data"]?.schema)) {
101
- op.payload = generator.addSchema(`${schemaId}RequestFormData`, operation.requestBody.content["multipart/form-data"].schema);
102
- op.payloadFormData = true;
276
+ const content = Predicate.isObject(response.content) ? response.content : undefined;
277
+ if (isHttpApi && Predicate.isNotUndefined(response.headers)) {
278
+ warnForOperation(emitWarning, op, {
279
+ code: "response-headers-ignored",
280
+ message: `Response headers on status ${status} were ignored in HttpApi generation.`
281
+ });
103
282
  }
104
- let defaultSchema;
105
- for (const entry of Object.entries(operation.responses ?? {})) {
106
- const status = entry[0];
107
- let response = entry[1];
108
- while ("$ref" in response) {
109
- response = resolveRef(response.$ref);
283
+ const representable = [];
284
+ let jsonSchemaName;
285
+ const jsonResponseSchema = content?.["application/json"]?.schema;
286
+ if (Predicate.isNotUndefined(jsonResponseSchema)) {
287
+ jsonSchemaName = addSchema(`${schemaId}${status}`, jsonResponseSchema, op);
288
+ if (isHttpApi) {
289
+ representable.push({
290
+ contentType: "application/json",
291
+ encoding: "json",
292
+ schema: jsonSchemaName
293
+ });
110
294
  }
111
- if (Predicate.isNotUndefined(response.content?.["application/json"]?.schema)) {
112
- const schemaName = generator.addSchema(`${schemaId}${status}`, response.content["application/json"].schema);
113
- if (status === "default") {
114
- defaultSchema = schemaName;
295
+ }
296
+ if (isHttpApi) {
297
+ for (const [contentType, mediaType] of Object.entries(content ?? {})) {
298
+ if (contentType === "application/json") {
115
299
  continue;
116
300
  }
117
- const statusLower = status.toLowerCase();
118
- const statusMajorNumber = Number(status[0]);
119
- if (Number.isNaN(statusMajorNumber)) {
301
+ if (!Predicate.isObject(mediaType) || Predicate.isUndefined(mediaType.schema)) {
120
302
  continue;
121
303
  }
122
- if (statusMajorNumber < 4) {
123
- op.successSchemas.set(statusLower, schemaName);
124
- } else {
125
- op.errorSchemas.set(statusLower, schemaName);
304
+ const encoding = getResponseMediaTypeEncoding(contentType);
305
+ if (encoding === undefined) {
306
+ continue;
126
307
  }
308
+ const schemaName = addSchema(`${schemaId}${status}${mediaTypeToSuffix(contentType)}`, mediaType.schema, op);
309
+ representable.push({
310
+ contentType,
311
+ encoding,
312
+ schema: schemaName
313
+ });
127
314
  }
128
- // Handle SSE streaming responses (text/event-stream)
129
- if (Predicate.isUndefined(op.sseSchema) && Predicate.isNotUndefined(response.content?.["text/event-stream"]?.schema)) {
130
- const statusMajorNumber = Number(status[0]);
131
- if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
132
- op.sseSchema = generator.addSchema(`${schemaId}${status}Sse`, response.content["text/event-stream"].schema);
133
- }
315
+ }
316
+ const isEmptyResponse = Predicate.isUndefined(content) || Object.keys(content).length === 0;
317
+ const parsedResponse = {
318
+ status: parsedStatus,
319
+ description: Utils.nonEmptyString(response.description),
320
+ contentTypes: Predicate.isNotUndefined(content) ? Object.keys(content) : [],
321
+ hasHeaders: Predicate.isNotUndefined(response.headers),
322
+ isEmpty: isEmptyResponse,
323
+ representable
324
+ };
325
+ op.responses.push(parsedResponse);
326
+ if (status === "default") {
327
+ op.defaultResponse = parsedResponse;
328
+ }
329
+ if (Predicate.isNotUndefined(jsonSchemaName)) {
330
+ const schemaName = jsonSchemaName;
331
+ if (status === "default" && !isHttpApi) {
332
+ defaultSchema = schemaName;
333
+ continue;
134
334
  }
135
- // Handle binary streaming responses (application/octet-stream)
136
- if (Predicate.isNotUndefined(response.content?.["application/octet-stream"])) {
137
- const statusMajorNumber = Number(status[0]);
138
- if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
139
- op.binaryResponse = true;
140
- }
335
+ const statusLower = parsedStatus.toLowerCase();
336
+ const statusMajorNumber = Number(parsedStatus[0]);
337
+ if (Number.isNaN(statusMajorNumber)) {
338
+ continue;
141
339
  }
142
- if (Predicate.isUndefined(response.content)) {
143
- if (status !== "default") {
144
- op.voidSchemas.add(status.toLowerCase());
145
- }
340
+ if (statusMajorNumber < 4) {
341
+ op.successSchemas.set(statusLower, schemaName);
342
+ } else {
343
+ op.errorSchemas.set(statusLower, schemaName);
344
+ }
345
+ }
346
+ const sseResponseSchema = content?.["text/event-stream"]?.schema;
347
+ if (Predicate.isUndefined(op.sseSchema) && Predicate.isNotUndefined(sseResponseSchema)) {
348
+ const statusMajorNumber = Number(parsedStatus[0]);
349
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
350
+ op.sseSchema = addSchema(`${schemaId}${status}Sse`, sseResponseSchema, op);
351
+ }
352
+ }
353
+ if (Predicate.isNotUndefined(content?.["application/octet-stream"])) {
354
+ const statusMajorNumber = Number(parsedStatus[0]);
355
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
356
+ op.binaryResponse = true;
146
357
  }
147
358
  }
148
- if (op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
149
- op.successSchemas.set("2xx", defaultSchema);
359
+ if (isEmptyResponse) {
360
+ if (parsedStatus !== "default") {
361
+ op.voidSchemas.add(parsedStatus.toLowerCase());
362
+ }
150
363
  }
151
- operations.push(op);
152
364
  }
365
+ if (!isHttpApi && op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
366
+ op.successSchemas.set("2xx", defaultSchema);
367
+ warnForOperation(emitWarning, op, {
368
+ code: "default-response-remapped",
369
+ message: "Default response was remapped to 2xx for the current HttpClient outputs."
370
+ });
371
+ }
372
+ operations.push(op);
153
373
  }
154
- for (const [path, methods] of Object.entries(spec.paths)) {
155
- handlePath(path, methods);
374
+ }
375
+ return {
376
+ metadata: {
377
+ title: spec.info.title,
378
+ version: spec.info.version,
379
+ summary: Utils.nonEmptyString(spec.info.summary),
380
+ description: Utils.nonEmptyString(spec.info.description),
381
+ license: spec.info.license,
382
+ servers: spec.servers
383
+ },
384
+ tags: (spec.tags ?? []).map(tag => ({
385
+ name: tag.name,
386
+ description: Utils.nonEmptyString(tag.description),
387
+ externalDocs: tag.externalDocs
388
+ })),
389
+ securitySchemes,
390
+ operations
391
+ };
392
+ };
393
+ const isOpenApiParameter = parameter => {
394
+ if (!Predicate.isObject(parameter)) {
395
+ return false;
396
+ }
397
+ return typeof parameter.name === "string" && (parameter.in === "path" || parameter.in === "query" || parameter.in === "header" || parameter.in === "cookie");
398
+ };
399
+ const resolveOperationParameters = (pathParameters, operationParameters, resolveRef) => {
400
+ const resolved = new Map();
401
+ const add = parameter => {
402
+ const current = resolveReference(parameter, resolveRef);
403
+ if (!isOpenApiParameter(current)) {
404
+ return;
156
405
  }
157
- // TODO: make a CLI option ?
158
- const importName = "Schema";
159
- const source = getDialect(spec);
160
- const generation = generator.generate(source, spec.components?.schemas ?? {}, options.typeOnly, {
161
- onEnter: options.onEnter
162
- });
163
- return String.stripMargin(`|${openApiTransformer.imports(importName, operations)}
164
- |${generation}
165
- |${openApiTransformer.toImplementation(importName, options.name, operations)}
166
- |
167
- |${openApiTransformer.toTypes(importName, options.name, operations)}`);
168
- }, (effect, _, options) => Effect.provideServiceEffect(effect, OpenApiTransformer.OpenApiTransformer, options.typeOnly ? Effect.sync(OpenApiTransformer.makeTransformerTs) : Effect.sync(OpenApiTransformer.makeTransformerSchema)));
406
+ const key = `${current.in}:${current.name}`;
407
+ if (resolved.has(key)) {
408
+ resolved.delete(key);
409
+ }
410
+ resolved.set(key, current);
411
+ };
412
+ for (const parameter of pathParameters ?? []) {
413
+ add(parameter);
414
+ }
415
+ for (const parameter of operationParameters ?? []) {
416
+ add(parameter);
417
+ }
418
+ return [...resolved.values()];
419
+ };
420
+ const buildParameterSchema = (parameters, onAdded) => {
421
+ if (parameters.length === 0) {
422
+ return;
423
+ }
424
+ const schema = {
425
+ type: "object",
426
+ properties: {},
427
+ required: [],
428
+ additionalProperties: false
429
+ };
430
+ for (const parameter of parameters) {
431
+ const paramSchema = parameter.schema;
432
+ const added = [];
433
+ if (Predicate.isObject(paramSchema) && "properties" in paramSchema && Predicate.isObject(paramSchema.properties)) {
434
+ const required = "required" in paramSchema ? paramSchema.required : [];
435
+ for (const [name, propertySchema] of Object.entries(paramSchema.properties)) {
436
+ const adjustedName = `${parameter.name}[${name}]`;
437
+ schema.properties[adjustedName] = propertySchema;
438
+ if (required.includes(name)) {
439
+ schema.required.push(adjustedName);
440
+ }
441
+ added.push(adjustedName);
442
+ }
443
+ } else {
444
+ schema.properties[parameter.name] = parameter.schema;
445
+ if (parameter.required) {
446
+ schema.required.push(parameter.name);
447
+ }
448
+ added.push(parameter.name);
449
+ }
450
+ onAdded?.(parameter, added);
451
+ }
169
452
  return {
170
- generate
453
+ schema,
454
+ optional: schema.required.length === 0
171
455
  };
172
- });
456
+ };
457
+ const mediaTypeToSuffix = contentType => {
458
+ const normalized = contentType.toLowerCase();
459
+ switch (normalized) {
460
+ case "application/json":
461
+ return "Json";
462
+ case "multipart/form-data":
463
+ return "FormData";
464
+ case "application/x-www-form-urlencoded":
465
+ return "FormUrlEncoded";
466
+ case "text/plain":
467
+ return "Text";
468
+ case "application/octet-stream":
469
+ return "Binary";
470
+ }
471
+ const suffix = Utils.identifier(contentType);
472
+ return suffix.length > 0 ? suffix : "Body";
473
+ };
474
+ const makeHttpApiMultipartSchemaRefs = definitions => {
475
+ const names = new Set(Object.keys(definitions));
476
+ const allocate = base => {
477
+ let candidate = base;
478
+ let index = 2;
479
+ while (names.has(candidate)) {
480
+ candidate = `${base}${index}`;
481
+ index += 1;
482
+ }
483
+ names.add(candidate);
484
+ return candidate;
485
+ };
486
+ return {
487
+ singleFile: allocate("__HttpApiMultipartSingleFile"),
488
+ files: allocate("__HttpApiMultipartFiles")
489
+ };
490
+ };
491
+ const toDefinitionRef = name => `#/$defs/${name.replaceAll("~", "~0").replaceAll("/", "~1")}`;
492
+ const withHttpApiMultipartSchemas = (definitions, multipartSchemaRefs) => {
493
+ if (multipartSchemaRefs === undefined) {
494
+ return definitions;
495
+ }
496
+ return {
497
+ ...definitions,
498
+ [multipartSchemaRefs.singleFile]: {
499
+ type: "string",
500
+ format: "binary"
501
+ },
502
+ [multipartSchemaRefs.files]: {
503
+ type: "array",
504
+ items: {
505
+ $ref: toDefinitionRef(multipartSchemaRefs.singleFile)
506
+ }
507
+ }
508
+ };
509
+ };
510
+ const transformMultipartSchema = (schema, multipartSchemaRefs, resolveRef) => {
511
+ if (multipartSchemaRefs === undefined) {
512
+ return schema;
513
+ }
514
+ const singleFileRef = toDefinitionRef(multipartSchemaRefs.singleFile);
515
+ const filesRef = toDefinitionRef(multipartSchemaRefs.files);
516
+ const cache = new Map();
517
+ const stack = new Set();
518
+ const visit = value => {
519
+ if (Array.isArray(value)) {
520
+ return value.map(visit);
521
+ }
522
+ if (!Predicate.isObject(value)) {
523
+ return value;
524
+ }
525
+ if (typeof value.$ref === "string" && value.$ref.startsWith("#/components/schemas/")) {
526
+ const cached = cache.get(value.$ref);
527
+ if (cached !== undefined) {
528
+ return cached;
529
+ }
530
+ if (stack.has(value.$ref)) {
531
+ return value;
532
+ }
533
+ stack.add(value.$ref);
534
+ const transformed = visit(resolveSchemaReference(value.$ref, resolveRef));
535
+ stack.delete(value.$ref);
536
+ cache.set(value.$ref, transformed);
537
+ return transformed;
538
+ }
539
+ if (isMultipartBinaryFile(value)) {
540
+ return {
541
+ $ref: singleFileRef
542
+ };
543
+ }
544
+ const out = {};
545
+ for (const [key, current] of Object.entries(value)) {
546
+ out[key] = visit(current);
547
+ }
548
+ if (isMultipartBinaryFiles(out, singleFileRef)) {
549
+ return {
550
+ $ref: filesRef
551
+ };
552
+ }
553
+ return out;
554
+ };
555
+ return visit(schema);
556
+ };
557
+ const resolveSchemaReference = (ref, resolveRef) => {
558
+ let current = {
559
+ $ref: ref
560
+ };
561
+ const seen = new Set();
562
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
563
+ if (seen.has(current.$ref)) {
564
+ return current;
565
+ }
566
+ seen.add(current.$ref);
567
+ current = resolveRef(current.$ref);
568
+ }
569
+ return current;
570
+ };
571
+ const isMultipartBinaryFile = value => Predicate.isObject(value) && value.type === "string" && (typeof value.format === "string" && value.format.toLowerCase() === "binary" || typeof value.contentEncoding === "string" && value.contentEncoding.toLowerCase() === "binary");
572
+ const isMultipartBinaryFiles = (value, singleFileRef) => {
573
+ if (value.type !== "array") {
574
+ return false;
575
+ }
576
+ const items = value.items;
577
+ return isMultipartBinaryFile(items) || Predicate.isObject(items) && items.$ref === singleFileRef;
578
+ };
579
+ const isJsonMediaType = contentType => contentType === "application/json" || contentType.startsWith("application/") && contentType.endsWith("+json");
580
+ const isTextMediaType = contentType => contentType.startsWith("text/");
581
+ const isBinaryMediaType = contentType => contentType === "application/octet-stream" || contentType.startsWith("application/") && (contentType.includes("binary") || contentType.endsWith("+octet-stream"));
582
+ const getRequestMediaTypeEncoding = contentType => {
583
+ const normalized = contentType.toLowerCase();
584
+ if (isJsonMediaType(normalized)) {
585
+ return "json";
586
+ }
587
+ if (normalized === "multipart/form-data") {
588
+ return "multipart";
589
+ }
590
+ if (normalized === "application/x-www-form-urlencoded") {
591
+ return "form-url-encoded";
592
+ }
593
+ if (isTextMediaType(normalized)) {
594
+ return "text";
595
+ }
596
+ if (isBinaryMediaType(normalized)) {
597
+ return "binary";
598
+ }
599
+ return;
600
+ };
601
+ const getResponseMediaTypeEncoding = contentType => {
602
+ const normalized = contentType.toLowerCase();
603
+ if (isJsonMediaType(normalized)) {
604
+ return "json";
605
+ }
606
+ if (normalized === "application/x-www-form-urlencoded") {
607
+ return "form-url-encoded";
608
+ }
609
+ if (isTextMediaType(normalized)) {
610
+ return "text";
611
+ }
612
+ if (isBinaryMediaType(normalized)) {
613
+ return "binary";
614
+ }
615
+ return;
616
+ };
617
+ const resolveReference = (input, resolveRef) => {
618
+ let current = input;
619
+ while (Predicate.isObject(current) && typeof current.$ref === "string") {
620
+ current = resolveRef(current.$ref);
621
+ }
622
+ return current;
623
+ };
624
+ const cloneSecurityRequirements = security => security.map(requirement => Object.fromEntries(Object.entries(requirement).map(([name, scopes]) => [name, [...scopes]])));
625
+ const parseSecuritySchemes = (spec, resolveRef) => {
626
+ const securitySchemes = spec.components?.securitySchemes ?? {};
627
+ const parsed = [];
628
+ for (const [name, value] of Object.entries(securitySchemes)) {
629
+ const scheme = resolveReference(value, resolveRef);
630
+ if (!Predicate.isObject(scheme)) {
631
+ continue;
632
+ }
633
+ if (scheme.type === "http") {
634
+ const normalizedScheme = scheme.scheme.toLowerCase();
635
+ if (normalizedScheme === "basic") {
636
+ parsed.push({
637
+ name,
638
+ type: "basic",
639
+ description: Utils.nonEmptyString(scheme.description),
640
+ bearerFormat: undefined,
641
+ key: undefined,
642
+ in: undefined
643
+ });
644
+ } else if (normalizedScheme === "bearer") {
645
+ parsed.push({
646
+ name,
647
+ type: "bearer",
648
+ description: Utils.nonEmptyString(scheme.description),
649
+ bearerFormat: Utils.nonEmptyString(scheme.bearerFormat),
650
+ key: undefined,
651
+ in: undefined
652
+ });
653
+ }
654
+ continue;
655
+ }
656
+ if (scheme.type === "apiKey" && (scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie") && typeof scheme.name === "string") {
657
+ parsed.push({
658
+ name,
659
+ type: "apiKey",
660
+ description: Utils.nonEmptyString(scheme.description),
661
+ bearerFormat: undefined,
662
+ key: scheme.name,
663
+ in: scheme.in
664
+ });
665
+ }
666
+ }
667
+ return parsed;
668
+ };
669
+ const warnForAndSecurityRequirements = (emitWarning, operation) => {
670
+ if (operation.effectiveSecurity.some(requirement => Object.keys(requirement).length === 0)) {
671
+ return;
672
+ }
673
+ for (const requirement of operation.effectiveSecurity) {
674
+ const schemes = Object.keys(requirement);
675
+ if (schemes.length <= 1) {
676
+ continue;
677
+ }
678
+ warnForOperation(emitWarning, operation, {
679
+ code: "security-and-downgraded",
680
+ message: `Security requirement requiring all of [${schemes.join(", ")}] was downgraded to a placeholder middleware.`
681
+ });
682
+ }
683
+ };
684
+ const hasSuccessfulSseResponse = (responses, hasExplicitSuccessResponse) => {
685
+ for (const [status, response] of responses) {
686
+ if (!Predicate.isObject(response)) {
687
+ continue;
688
+ }
689
+ const content = Predicate.isObject(response.content) ? response.content : undefined;
690
+ if (Predicate.isUndefined(content?.["text/event-stream"]?.schema)) {
691
+ continue;
692
+ }
693
+ const remappedStatus = remapDefaultResponseStatusForHttpApi(status, hasExplicitSuccessResponse);
694
+ const statusCode = Number(remappedStatus);
695
+ if (!Number.isNaN(statusCode) && statusCode < 400) {
696
+ return true;
697
+ }
698
+ }
699
+ return false;
700
+ };
701
+ const remapDefaultResponseStatusForHttpApi = (status, hasExplicitSuccessResponse) => status === "default" ? hasExplicitSuccessResponse ? "500" : "200" : status;
702
+ const methodSupportsRequestBody = method => method !== "get" && method !== "head" && method !== "options" && method !== "trace";
703
+ const warnForOperation = (emitWarning, operation, warning) => {
704
+ emitWarning({
705
+ ...warning,
706
+ path: operation.path,
707
+ method: operation.method,
708
+ operationId: operation.operationId
709
+ });
710
+ };
173
711
  function getDialect(spec) {
174
712
  return spec.openapi.trim().startsWith("3.0") ? "openapi-3.0" : "openapi-3.1";
175
713
  }