@effect/openapi-generator 4.0.0-beta.0

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/JsonSchemaGenerator.d.ts +8 -0
  3. package/dist/JsonSchemaGenerator.d.ts.map +1 -0
  4. package/dist/JsonSchemaGenerator.js +75 -0
  5. package/dist/JsonSchemaGenerator.js.map +1 -0
  6. package/dist/OpenApiGenerator.d.ts +32 -0
  7. package/dist/OpenApiGenerator.d.ts.map +1 -0
  8. package/dist/OpenApiGenerator.js +206 -0
  9. package/dist/OpenApiGenerator.js.map +1 -0
  10. package/dist/OpenApiPatch.d.ts +296 -0
  11. package/dist/OpenApiPatch.d.ts.map +1 -0
  12. package/dist/OpenApiPatch.js +448 -0
  13. package/dist/OpenApiPatch.js.map +1 -0
  14. package/dist/OpenApiTransformer.d.ts +24 -0
  15. package/dist/OpenApiTransformer.d.ts.map +1 -0
  16. package/dist/OpenApiTransformer.js +740 -0
  17. package/dist/OpenApiTransformer.js.map +1 -0
  18. package/dist/ParsedOperation.d.ts +29 -0
  19. package/dist/ParsedOperation.d.ts.map +1 -0
  20. package/dist/ParsedOperation.js +13 -0
  21. package/dist/ParsedOperation.js.map +1 -0
  22. package/dist/Utils.d.ts +6 -0
  23. package/dist/Utils.d.ts.map +1 -0
  24. package/dist/Utils.js +42 -0
  25. package/dist/Utils.js.map +1 -0
  26. package/dist/bin.d.ts +3 -0
  27. package/dist/bin.d.ts.map +1 -0
  28. package/dist/bin.js +7 -0
  29. package/dist/bin.js.map +1 -0
  30. package/dist/main.d.ts +5 -0
  31. package/dist/main.d.ts.map +1 -0
  32. package/dist/main.js +47 -0
  33. package/dist/main.js.map +1 -0
  34. package/package.json +67 -0
  35. package/src/JsonSchemaGenerator.ts +94 -0
  36. package/src/OpenApiGenerator.ts +309 -0
  37. package/src/OpenApiPatch.ts +514 -0
  38. package/src/OpenApiTransformer.ts +954 -0
  39. package/src/ParsedOperation.ts +43 -0
  40. package/src/Utils.ts +50 -0
  41. package/src/bin.ts +10 -0
  42. package/src/main.ts +68 -0
@@ -0,0 +1,954 @@
1
+ import * as Layer from "effect/Layer"
2
+ import * as Predicate from "effect/Predicate"
3
+ import * as ServiceMap from "effect/ServiceMap"
4
+ import type { OpenAPISpecMethodName } from "effect/unstable/httpapi/OpenApi"
5
+ import type { ParsedOperation } from "./ParsedOperation.ts"
6
+ import * as Utils from "./Utils.ts"
7
+
8
+ export class OpenApiTransformer extends ServiceMap.Service<
9
+ OpenApiTransformer,
10
+ {
11
+ readonly imports: (importName: string, operations: ReadonlyArray<ParsedOperation>) => string
12
+ readonly toTypes: (importName: string, name: string, operations: ReadonlyArray<ParsedOperation>) => string
13
+ readonly toImplementation: (importName: string, name: string, operations: ReadonlyArray<ParsedOperation>) => string
14
+ }
15
+ >()("OpenApiTransformer") {}
16
+
17
+ interface ImportRequirements {
18
+ readonly eventStream: boolean
19
+ readonly octetStream: boolean
20
+ }
21
+
22
+ const computeImportRequirements = (operations: ReadonlyArray<ParsedOperation>): ImportRequirements => {
23
+ let eventStream = false
24
+ let octetStream = false
25
+ for (const op of operations) {
26
+ if (op.sseSchema) {
27
+ eventStream = true
28
+ }
29
+ if (op.binaryResponse) {
30
+ octetStream = true
31
+ }
32
+ }
33
+ return { eventStream, octetStream }
34
+ }
35
+
36
+ const requiresStreaming = (requirements: ImportRequirements): boolean =>
37
+ requirements.eventStream || requirements.octetStream
38
+
39
+ const httpClientMethodNames: Record<OpenAPISpecMethodName, string> = {
40
+ get: "get",
41
+ put: "put",
42
+ post: "post",
43
+ delete: "del",
44
+ options: "options",
45
+ head: "head",
46
+ patch: "patch",
47
+ trace: `make("TRACE")`
48
+ }
49
+
50
+ export const makeTransformerSchema = () => {
51
+ const operationsToInterface = (
52
+ _importName: string,
53
+ name: string,
54
+ operations: ReadonlyArray<ParsedOperation>
55
+ ) => {
56
+ const methods: Array<string> = []
57
+ for (const op of operations) {
58
+ methods.push(operationToMethod(name, op))
59
+ if (op.sseSchema) {
60
+ methods.push(operationToSseMethod(name, op))
61
+ }
62
+ if (op.binaryResponse) {
63
+ methods.push(operationToBinaryMethod(name, op))
64
+ }
65
+ }
66
+ return `export interface ${name} {
67
+ readonly httpClient: HttpClient.HttpClient
68
+ ${methods.join("\n ")}
69
+ }
70
+
71
+ ${clientErrorSource(name)}`
72
+ }
73
+
74
+ const operationToMethod = (name: string, operation: ParsedOperation) => {
75
+ const args: Array<string> = []
76
+ if (operation.pathIds.length > 0) {
77
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
78
+ }
79
+
80
+ const options: Array<string> = []
81
+ if (operation.params) {
82
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
83
+ const type = `typeof ${operation.params}.Encoded${operation.paramsOptional ? " | undefined" : ""}`
84
+ options.push(`${key}: ${type}`)
85
+ }
86
+ if (operation.payload) {
87
+ const key = `readonly payload`
88
+ const type = `typeof ${operation.payload}.Encoded`
89
+ options.push(`${key}: ${type}`)
90
+ }
91
+ options.push("readonly config?: Config | undefined")
92
+
93
+ // If all options are optional, the argument itself should be optional
94
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
95
+ if (hasOptions) {
96
+ args.push(`options: { ${options.join("; ")} }`)
97
+ } else {
98
+ args.push(`options: { ${options.join("; ")} } | undefined`)
99
+ }
100
+
101
+ let success = "void"
102
+ if (operation.successSchemas.size > 0) {
103
+ success = Array.from(operation.successSchemas.values())
104
+ .map((schema) => `typeof ${schema}.Type`)
105
+ .join(" | ")
106
+ }
107
+ const errors = ["HttpClientError.HttpClientError", "SchemaError"]
108
+ if (operation.errorSchemas.size > 0) {
109
+ Utils.spreadElementsInto(
110
+ Array.from(operation.errorSchemas.values()).map(
111
+ (schema) => `${name}Error<"${schema}", typeof ${schema}.Type>`
112
+ ),
113
+ errors
114
+ )
115
+ }
116
+
117
+ const jsdoc = Utils.toComment(operation.description)
118
+ const methodKey = `readonly "${operation.id}"`
119
+ const generic = `<Config extends OperationConfig>`
120
+ const parameters = args.join(", ")
121
+ const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}>`
122
+ return `${jsdoc}${methodKey}: ${generic}(${parameters}) => ${returnType}`
123
+ }
124
+
125
+ const operationToSseMethod = (_name: string, operation: ParsedOperation) => {
126
+ const args: Array<string> = []
127
+ if (operation.pathIds.length > 0) {
128
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
129
+ }
130
+
131
+ const options: Array<string> = []
132
+ if (operation.params) {
133
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
134
+ const type = `typeof ${operation.params}.Encoded${operation.paramsOptional ? " | undefined" : ""}`
135
+ options.push(`${key}: ${type}`)
136
+ }
137
+ if (operation.payload) {
138
+ options.push(`readonly payload: typeof ${operation.payload}.Encoded`)
139
+ }
140
+
141
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
142
+ if (hasOptions) {
143
+ args.push(`options: { ${options.join("; ")} }`)
144
+ } else if (options.length > 0) {
145
+ args.push(`options: { ${options.join("; ")} } | undefined`)
146
+ }
147
+
148
+ const jsdoc = Utils.toComment(operation.description)
149
+ const methodKey = `readonly "${operation.id}Sse"`
150
+ const parameters = args.join(", ")
151
+ const returnType =
152
+ `Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof ${operation.sseSchema}.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof ${operation.sseSchema}.DecodingServices>`
153
+ return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
154
+ }
155
+
156
+ const operationToBinaryMethod = (_name: string, operation: ParsedOperation) => {
157
+ const args: Array<string> = []
158
+ if (operation.pathIds.length > 0) {
159
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
160
+ }
161
+
162
+ const options: Array<string> = []
163
+ if (operation.params) {
164
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
165
+ const type = `typeof ${operation.params}.Encoded${operation.paramsOptional ? " | undefined" : ""}`
166
+ options.push(`${key}: ${type}`)
167
+ }
168
+ if (operation.payload) {
169
+ options.push(`readonly payload: typeof ${operation.payload}.Encoded`)
170
+ }
171
+
172
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
173
+ if (hasOptions) {
174
+ args.push(`options: { ${options.join("; ")} }`)
175
+ } else if (options.length > 0) {
176
+ args.push(`options: { ${options.join("; ")} } | undefined`)
177
+ }
178
+
179
+ const jsdoc = Utils.toComment(operation.description)
180
+ const methodKey = `readonly "${operation.id}Stream"`
181
+ const parameters = args.join(", ")
182
+ const returnType = `Stream.Stream<Uint8Array, HttpClientError.HttpClientError>`
183
+ return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
184
+ }
185
+
186
+ const operationsToImpl = (
187
+ importName: string,
188
+ name: string,
189
+ operations: ReadonlyArray<ParsedOperation>
190
+ ) => {
191
+ const requirements = computeImportRequirements(operations)
192
+ const implMethods: Array<string> = []
193
+ for (const op of operations) {
194
+ implMethods.push(operationToImpl(op))
195
+ if (op.sseSchema) {
196
+ implMethods.push(operationToSseImpl(importName, op))
197
+ }
198
+ if (op.binaryResponse) {
199
+ implMethods.push(operationToBinaryImpl(op))
200
+ }
201
+ }
202
+
203
+ const helpers: Array<string> = [commonSource]
204
+ if (requirements.eventStream) {
205
+ helpers.push(sseRequestSource(importName))
206
+ }
207
+ if (requirements.octetStream) {
208
+ helpers.push(binaryRequestSource)
209
+ }
210
+
211
+ return `export interface OperationConfig {
212
+ /**
213
+ * Whether or not the response should be included in the value returned from
214
+ * an operation.
215
+ *
216
+ * If set to \`true\`, a tuple of \`[A, HttpClientResponse]\` will be returned,
217
+ * where \`A\` is the success type of the operation.
218
+ *
219
+ * If set to \`false\`, only the success type of the operation will be returned.
220
+ */
221
+ readonly includeResponse?: boolean | undefined
222
+ }
223
+
224
+ /**
225
+ * A utility type which optionally includes the response in the return result
226
+ * of an operation based upon the value of the \`includeResponse\` configuration
227
+ * option.
228
+ */
229
+ export type WithOptionalResponse<A, Config extends OperationConfig> = Config extends {
230
+ readonly includeResponse: true
231
+ } ? [A, HttpClientResponse.HttpClientResponse] : A
232
+
233
+ export const make = (
234
+ httpClient: HttpClient.HttpClient,
235
+ options: {
236
+ readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
237
+ } = {}
238
+ ): ${name} => {
239
+ ${helpers.join("\n ")}
240
+ const decodeSuccess =
241
+ <Schema extends ${importName}.Top>(schema: Schema) =>
242
+ (response: HttpClientResponse.HttpClientResponse) =>
243
+ HttpClientResponse.schemaBodyJson(schema)(response)
244
+ const decodeError =
245
+ <const Tag extends string, Schema extends ${importName}.Top>(tag: Tag, schema: Schema) =>
246
+ (response: HttpClientResponse.HttpClientResponse) =>
247
+ Effect.flatMap(
248
+ HttpClientResponse.schemaBodyJson(schema)(response),
249
+ (cause) => Effect.fail(${name}Error(tag, cause, response)),
250
+ )
251
+ return {
252
+ httpClient,
253
+ ${implMethods.join(",\n ")}
254
+ }
255
+ }`
256
+ }
257
+
258
+ const operationToImpl = (operation: ParsedOperation) => {
259
+ const args: Array<string> = [...operation.pathIds, "options"]
260
+ const params = `${args.join(", ")}`
261
+
262
+ const pipeline: Array<string> = []
263
+
264
+ if (operation.params) {
265
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
266
+
267
+ if (operation.urlParams.length > 0) {
268
+ const props = operation.urlParams.map(
269
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
270
+ )
271
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
272
+ }
273
+ if (operation.headers.length > 0) {
274
+ const props = operation.headers.map(
275
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
276
+ )
277
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
278
+ }
279
+ }
280
+
281
+ const payloadVarName = "options.payload"
282
+ if (operation.payloadFormData) {
283
+ pipeline.push(`HttpClientRequest.bodyFormData(${payloadVarName} as any)`)
284
+ } else if (operation.payload) {
285
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(${payloadVarName})`)
286
+ }
287
+
288
+ const decodes: Array<string> = []
289
+ const singleSuccessCode = operation.successSchemas.size === 1
290
+ operation.successSchemas.forEach((schema, status) => {
291
+ const statusCode = singleSuccessCode && status.startsWith("2") ? "2xx" : status
292
+ decodes.push(`"${statusCode}": decodeSuccess(${schema})`)
293
+ })
294
+ operation.errorSchemas.forEach((schema, status) => {
295
+ decodes.push(`"${status}": decodeError("${schema}", ${schema})`)
296
+ })
297
+ operation.voidSchemas.forEach((status) => {
298
+ decodes.push(`"${status}": () => Effect.void`)
299
+ })
300
+ decodes.push(`orElse: unexpectedStatus`)
301
+
302
+ const configAccessor = resolveConfigAccessor(operation, "options", "config")
303
+ pipeline.push(`withResponse(${configAccessor})(HttpClientResponse.matchStatus({
304
+ ${decodes.join(",\n ")}
305
+ }))`)
306
+
307
+ return (
308
+ `"${operation.id}": (${params}) => ` +
309
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
310
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
311
+ )
312
+ }
313
+
314
+ const operationToSseImpl = (_importName: string, operation: ParsedOperation) => {
315
+ const args: Array<string> = [...operation.pathIds]
316
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
317
+ if (hasOptions || operation.params || operation.payload) {
318
+ args.push("options")
319
+ }
320
+ const params = args.join(", ")
321
+
322
+ const pipeline: Array<string> = []
323
+
324
+ if (operation.params) {
325
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
326
+ if (operation.urlParams.length > 0) {
327
+ const props = operation.urlParams.map(
328
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
329
+ )
330
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
331
+ }
332
+ if (operation.headers.length > 0) {
333
+ const props = operation.headers.map(
334
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
335
+ )
336
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
337
+ }
338
+ }
339
+
340
+ if (operation.payloadFormData) {
341
+ pipeline.push(`HttpClientRequest.bodyFormData(options.payload as any)`)
342
+ } else if (operation.payload) {
343
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(options.payload)`)
344
+ }
345
+
346
+ pipeline.push(`sseRequest(${operation.sseSchema})`)
347
+
348
+ return (
349
+ `"${operation.id}Sse": (${params}) => ` +
350
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
351
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
352
+ )
353
+ }
354
+
355
+ const operationToBinaryImpl = (operation: ParsedOperation) => {
356
+ const args: Array<string> = [...operation.pathIds]
357
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
358
+ if (hasOptions || operation.params || operation.payload) {
359
+ args.push("options")
360
+ }
361
+ const params = args.join(", ")
362
+
363
+ const pipeline: Array<string> = []
364
+
365
+ if (operation.params) {
366
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
367
+ if (operation.urlParams.length > 0) {
368
+ const props = operation.urlParams.map(
369
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
370
+ )
371
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
372
+ }
373
+ if (operation.headers.length > 0) {
374
+ const props = operation.headers.map(
375
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
376
+ )
377
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
378
+ }
379
+ }
380
+
381
+ if (operation.payloadFormData) {
382
+ pipeline.push(`HttpClientRequest.bodyFormData(options.payload as any)`)
383
+ } else if (operation.payload) {
384
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(options.payload)`)
385
+ }
386
+
387
+ pipeline.push(`binaryRequest`)
388
+
389
+ return (
390
+ `"${operation.id}Stream": (${params}) => ` +
391
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
392
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
393
+ )
394
+ }
395
+
396
+ return OpenApiTransformer.of({
397
+ imports: (importName, operations) => {
398
+ const requirements = computeImportRequirements(operations)
399
+ const imports = [
400
+ `import * as Data from "effect/Data"`,
401
+ `import * as Effect from "effect/Effect"`,
402
+ `import type { SchemaError } from "effect/Schema"`,
403
+ `import * as ${importName} from "effect/Schema"`
404
+ ]
405
+ if (requiresStreaming(requirements)) {
406
+ imports.push(`import * as Stream from "effect/Stream"`)
407
+ }
408
+ if (requirements.eventStream) {
409
+ imports.push(`import * as Sse from "effect/unstable/encoding/Sse"`)
410
+ }
411
+ // HttpClient needs to be a value import when streaming is used (for filterStatusOk)
412
+ if (requiresStreaming(requirements)) {
413
+ imports.push(`import * as HttpClient from "effect/unstable/http/HttpClient"`)
414
+ } else {
415
+ imports.push(`import type * as HttpClient from "effect/unstable/http/HttpClient"`)
416
+ }
417
+ imports.push(
418
+ `import * as HttpClientError from "effect/unstable/http/HttpClientError"`,
419
+ `import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"`,
420
+ `import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"`
421
+ )
422
+ return imports.join("\n")
423
+ },
424
+ toTypes: operationsToInterface,
425
+ toImplementation: operationsToImpl
426
+ })
427
+ }
428
+
429
+ export const layerTransformerSchema = Layer.sync(
430
+ OpenApiTransformer,
431
+ makeTransformerSchema
432
+ )
433
+
434
+ export const makeTransformerTs = () => {
435
+ const operationsToInterface = (
436
+ _importName: string,
437
+ name: string,
438
+ operations: ReadonlyArray<ParsedOperation>
439
+ ) => {
440
+ const methods: Array<string> = []
441
+ for (const op of operations) {
442
+ methods.push(operationToMethod(name, op))
443
+ if (op.sseSchema) {
444
+ methods.push(operationToSseMethod(op))
445
+ }
446
+ if (op.binaryResponse) {
447
+ methods.push(operationToBinaryMethod(op))
448
+ }
449
+ }
450
+ return `export interface ${name} {
451
+ readonly httpClient: HttpClient.HttpClient
452
+ ${methods.join("\n ")}
453
+ }
454
+
455
+ ${clientErrorSource(name)}`
456
+ }
457
+
458
+ const operationToMethod = (name: string, operation: ParsedOperation) => {
459
+ const args: Array<string> = []
460
+ if (operation.pathIds.length > 0) {
461
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
462
+ }
463
+
464
+ const options: Array<string> = []
465
+ if (operation.params) {
466
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
467
+ const type = `${operation.params}${operation.paramsOptional ? " | undefined" : ""}`
468
+ options.push(`${key}: ${type}`)
469
+ }
470
+ if (operation.payload) {
471
+ options.push(`readonly payload: ${operation.payload}`)
472
+ }
473
+ options.push("readonly config?: Config | undefined")
474
+
475
+ // If all options are optional, the argument itself should be optional
476
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
477
+ if (hasOptions) {
478
+ args.push(`options: { ${options.join("; ")} }`)
479
+ } else {
480
+ args.push(`options: { ${options.join("; ")} } | undefined`)
481
+ }
482
+
483
+ let success = "void"
484
+ if (operation.successSchemas.size > 0) {
485
+ success = Array.from(operation.successSchemas.values()).join(" | ")
486
+ }
487
+
488
+ const errors = ["HttpClientError.HttpClientError"]
489
+ if (operation.errorSchemas.size > 0) {
490
+ for (const schema of operation.errorSchemas.values()) {
491
+ errors.push(`${name}Error<"${schema}", ${schema}>`)
492
+ }
493
+ }
494
+
495
+ const jsdoc = Utils.toComment(operation.description)
496
+ const methodKey = `readonly "${operation.id}"`
497
+ const generic = `<Config extends OperationConfig>`
498
+ const parameters = args.join(", ")
499
+ const returnType = `Effect.Effect<WithOptionalResponse<${success}, Config>, ${errors.join(" | ")}>`
500
+ return `${jsdoc}${methodKey}: ${generic}(${parameters}) => ${returnType}`
501
+ }
502
+
503
+ const operationToSseMethod = (operation: ParsedOperation) => {
504
+ const args: Array<string> = []
505
+ if (operation.pathIds.length > 0) {
506
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
507
+ }
508
+
509
+ const options: Array<string> = []
510
+ if (operation.params) {
511
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
512
+ const type = `${operation.params}${operation.paramsOptional ? " | undefined" : ""}`
513
+ options.push(`${key}: ${type}`)
514
+ }
515
+ if (operation.payload) {
516
+ options.push(`readonly payload: ${operation.payload}`)
517
+ }
518
+
519
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
520
+ if (hasOptions) {
521
+ args.push(`options: { ${options.join("; ")} }`)
522
+ } else if (options.length > 0) {
523
+ args.push(`options: { ${options.join("; ")} } | undefined`)
524
+ }
525
+
526
+ const jsdoc = Utils.toComment(operation.description)
527
+ const methodKey = `readonly "${operation.id}Sse"`
528
+ const parameters = args.join(", ")
529
+ const returnType = `Stream.Stream<${operation.sseSchema}, HttpClientError.HttpClientError>`
530
+ return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
531
+ }
532
+
533
+ const operationToBinaryMethod = (operation: ParsedOperation) => {
534
+ const args: Array<string> = []
535
+ if (operation.pathIds.length > 0) {
536
+ Utils.spreadElementsInto(operation.pathIds.map((id) => `${id}: string`), args)
537
+ }
538
+
539
+ const options: Array<string> = []
540
+ if (operation.params) {
541
+ const key = `readonly params${operation.paramsOptional ? "?" : ""}`
542
+ const type = `${operation.params}${operation.paramsOptional ? " | undefined" : ""}`
543
+ options.push(`${key}: ${type}`)
544
+ }
545
+ if (operation.payload) {
546
+ options.push(`readonly payload: ${operation.payload}`)
547
+ }
548
+
549
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
550
+ if (hasOptions) {
551
+ args.push(`options: { ${options.join("; ")} }`)
552
+ } else if (options.length > 0) {
553
+ args.push(`options: { ${options.join("; ")} } | undefined`)
554
+ }
555
+
556
+ const jsdoc = Utils.toComment(operation.description)
557
+ const methodKey = `readonly "${operation.id}Stream"`
558
+ const parameters = args.join(", ")
559
+ const returnType = `Stream.Stream<Uint8Array, HttpClientError.HttpClientError>`
560
+ return `${jsdoc}${methodKey}: (${parameters}) => ${returnType}`
561
+ }
562
+
563
+ const operationsToImpl = (
564
+ _importName: string,
565
+ name: string,
566
+ operations: ReadonlyArray<ParsedOperation>
567
+ ) => {
568
+ const requirements = computeImportRequirements(operations)
569
+ const implMethods: Array<string> = []
570
+ for (const op of operations) {
571
+ implMethods.push(operationToImpl(op))
572
+ if (op.sseSchema) {
573
+ implMethods.push(operationToSseImpl(op))
574
+ }
575
+ if (op.binaryResponse) {
576
+ implMethods.push(operationToBinaryImpl(op))
577
+ }
578
+ }
579
+
580
+ const helpers: Array<string> = [commonSource]
581
+ if (requirements.eventStream) {
582
+ helpers.push(sseRequestSourceTs)
583
+ }
584
+ if (requirements.octetStream) {
585
+ helpers.push(binaryRequestSourceTs)
586
+ }
587
+
588
+ return `export interface OperationConfig {
589
+ /**
590
+ * Whether or not the response should be included in the value returned from
591
+ * an operation.
592
+ *
593
+ * If set to \`true\`, a tuple of \`[A, HttpClientResponse]\` will be returned,
594
+ * where \`A\` is the success type of the operation.
595
+ *
596
+ * If set to \`false\`, only the success type of the operation will be returned.
597
+ */
598
+ readonly includeResponse?: boolean | undefined
599
+ }
600
+
601
+ /**
602
+ * A utility type which optionally includes the response in the return result
603
+ * of an operation based upon the value of the \`includeResponse\` configuration
604
+ * option.
605
+ */
606
+ export type WithOptionalResponse<A, Config extends OperationConfig> = Config extends {
607
+ readonly includeResponse: true
608
+ } ? [A, HttpClientResponse.HttpClientResponse] : A
609
+
610
+ export const make = (
611
+ httpClient: HttpClient.HttpClient,
612
+ options: {
613
+ readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect<HttpClient.HttpClient>) | undefined
614
+ } = {}
615
+ ): ${name} => {
616
+ ${helpers.join("\n ")}
617
+ const decodeSuccess = <A>(response: HttpClientResponse.HttpClientResponse) =>
618
+ response.json as Effect.Effect<A, HttpClientError.HttpClientError>
619
+ const decodeVoid = (_response: HttpClientResponse.HttpClientResponse) =>
620
+ Effect.void
621
+ const decodeError =
622
+ <Tag extends string, E>(tag: Tag) =>
623
+ (
624
+ response: HttpClientResponse.HttpClientResponse,
625
+ ): Effect.Effect<
626
+ never,
627
+ ${name}Error<Tag, E> | HttpClientError.HttpClientError
628
+ > =>
629
+ Effect.flatMap(
630
+ response.json as Effect.Effect<E, HttpClientError.HttpClientError>,
631
+ (cause) => Effect.fail(${name}Error(tag, cause, response)),
632
+ )
633
+ const onRequest = <Config extends OperationConfig>(config: Config | undefined) => (
634
+ successCodes: ReadonlyArray<string>,
635
+ errorCodes?: Record<string, string>,
636
+ ) => {
637
+ const cases: any = { orElse: unexpectedStatus }
638
+ for (const code of successCodes) {
639
+ cases[code] = decodeSuccess
640
+ }
641
+ if (errorCodes) {
642
+ for (const [code, tag] of Object.entries(errorCodes)) {
643
+ cases[code] = decodeError(tag)
644
+ }
645
+ }
646
+ if (successCodes.length === 0) {
647
+ cases["2xx"] = decodeVoid
648
+ }
649
+ return withResponse(config)(HttpClientResponse.matchStatus(cases) as any)
650
+ }
651
+ return {
652
+ httpClient,
653
+ ${implMethods.join(",\n ")}
654
+ }
655
+ }`
656
+ }
657
+
658
+ const operationToImpl = (operation: ParsedOperation) => {
659
+ const args: Array<string> = [...operation.pathIds, "options"]
660
+ const params = `${args.join(", ")}`
661
+
662
+ const pipeline: Array<string> = []
663
+
664
+ if (operation.params) {
665
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
666
+
667
+ if (operation.urlParams.length > 0) {
668
+ const props = operation.urlParams.map(
669
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
670
+ )
671
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
672
+ }
673
+ if (operation.headers.length > 0) {
674
+ const props = operation.headers.map(
675
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
676
+ )
677
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
678
+ }
679
+ }
680
+
681
+ const payloadAccessor = "options.payload"
682
+ if (operation.payloadFormData) {
683
+ pipeline.push(`HttpClientRequest.bodyFormDataRecord(${payloadAccessor} as any)`)
684
+ } else if (operation.payload) {
685
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(${payloadAccessor})`)
686
+ }
687
+
688
+ const successCodesRaw = Array.from(operation.successSchemas.keys())
689
+ const successCodes = successCodesRaw
690
+ .map((_) => JSON.stringify(_))
691
+ .join(", ")
692
+ const singleSuccessCode = successCodesRaw.length === 1 && successCodesRaw[0].startsWith("2")
693
+ const errorCodes = operation.errorSchemas.size > 0 &&
694
+ Object.fromEntries(operation.errorSchemas.entries())
695
+ const configAccessor = resolveConfigAccessor(operation, "options", "config")
696
+ pipeline.push(
697
+ `onRequest(${configAccessor})([${singleSuccessCode ? `"2xx"` : successCodes}]${
698
+ errorCodes ? `, ${JSON.stringify(errorCodes)}` : ""
699
+ })`
700
+ )
701
+
702
+ return (
703
+ `"${operation.id}": (${params}) => ` +
704
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
705
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
706
+ )
707
+ }
708
+
709
+ const operationToSseImpl = (operation: ParsedOperation) => {
710
+ const args: Array<string> = [...operation.pathIds]
711
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
712
+ if (hasOptions || operation.params || operation.payload) {
713
+ args.push("options")
714
+ }
715
+ const params = args.join(", ")
716
+
717
+ const pipeline: Array<string> = []
718
+
719
+ if (operation.params) {
720
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
721
+ if (operation.urlParams.length > 0) {
722
+ const props = operation.urlParams.map(
723
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
724
+ )
725
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
726
+ }
727
+ if (operation.headers.length > 0) {
728
+ const props = operation.headers.map(
729
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
730
+ )
731
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
732
+ }
733
+ }
734
+
735
+ if (operation.payloadFormData) {
736
+ pipeline.push(`HttpClientRequest.bodyFormDataRecord(options.payload as any)`)
737
+ } else if (operation.payload) {
738
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(options.payload)`)
739
+ }
740
+
741
+ pipeline.push(`sseRequest`)
742
+
743
+ return (
744
+ `"${operation.id}Sse": (${params}) => ` +
745
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
746
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
747
+ )
748
+ }
749
+
750
+ const operationToBinaryImpl = (operation: ParsedOperation) => {
751
+ const args: Array<string> = [...operation.pathIds]
752
+ const hasOptions = (operation.params && !operation.paramsOptional) || operation.payload
753
+ if (hasOptions || operation.params || operation.payload) {
754
+ args.push("options")
755
+ }
756
+ const params = args.join(", ")
757
+
758
+ const pipeline: Array<string> = []
759
+
760
+ if (operation.params) {
761
+ const paramsAccessor = resolveParamsAccessor(operation, "options", "params")
762
+ if (operation.urlParams.length > 0) {
763
+ const props = operation.urlParams.map(
764
+ (param) => `"${param}": ${paramsAccessor}["${param}"] as any`
765
+ )
766
+ pipeline.push(`HttpClientRequest.setUrlParams({ ${props.join(", ")} })`)
767
+ }
768
+ if (operation.headers.length > 0) {
769
+ const props = operation.headers.map(
770
+ (param) => `"${param}": ${paramsAccessor}["${param}"] ?? undefined`
771
+ )
772
+ pipeline.push(`HttpClientRequest.setHeaders({ ${props.join(", ")} })`)
773
+ }
774
+ }
775
+
776
+ if (operation.payloadFormData) {
777
+ pipeline.push(`HttpClientRequest.bodyFormDataRecord(options.payload as any)`)
778
+ } else if (operation.payload) {
779
+ pipeline.push(`HttpClientRequest.bodyJsonUnsafe(options.payload)`)
780
+ }
781
+
782
+ pipeline.push(`binaryRequest`)
783
+
784
+ return (
785
+ `"${operation.id}Stream": (${params}) => ` +
786
+ `HttpClientRequest.${httpClientMethodNames[operation.method]}(${operation.pathTemplate})` +
787
+ `.pipe(\n ${pipeline.join(",\n ")}\n )`
788
+ )
789
+ }
790
+
791
+ return OpenApiTransformer.of({
792
+ imports: (_importName, operations) => {
793
+ const requirements = computeImportRequirements(operations)
794
+ const imports = [
795
+ `import * as Data from "effect/Data"`,
796
+ `import * as Effect from "effect/Effect"`
797
+ ]
798
+ if (requiresStreaming(requirements)) {
799
+ imports.push(`import * as Stream from "effect/Stream"`)
800
+ }
801
+ imports.push(
802
+ `import type * as HttpClient from "effect/unstable/http/HttpClient"`,
803
+ `import * as HttpClientError from "effect/unstable/http/HttpClientError"`,
804
+ `import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"`,
805
+ `import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"`
806
+ )
807
+ return imports.join("\n")
808
+ },
809
+ toTypes: operationsToInterface,
810
+ toImplementation: operationsToImpl
811
+ })
812
+ }
813
+
814
+ export const layerTransformerTs = Layer.sync(
815
+ OpenApiTransformer,
816
+ makeTransformerTs
817
+ )
818
+
819
+ const commonSource = `const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) =>
820
+ Effect.flatMap(
821
+ Effect.orElseSucceed(response.json, () => "Unexpected status code"),
822
+ (description) =>
823
+ Effect.fail(
824
+ new HttpClientError.HttpClientError({
825
+ reason: new HttpClientError.StatusCodeError({
826
+ request: response.request,
827
+ response,
828
+ description: typeof description === "string" ? description : JSON.stringify(description),
829
+ }),
830
+ }),
831
+ ),
832
+ )
833
+ const withResponse = <Config extends OperationConfig>(config: Config | undefined) => (
834
+ f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<any, any>,
835
+ ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect<any, any> => {
836
+ const withOptionalResponse = (
837
+ config?.includeResponse
838
+ ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response])
839
+ : (response: HttpClientResponse.HttpClientResponse) => f(response)
840
+ ) as any
841
+ return options?.transformClient
842
+ ? (request) =>
843
+ Effect.flatMap(
844
+ Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)),
845
+ withOptionalResponse
846
+ )
847
+ : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse)
848
+ }`
849
+
850
+ const sseRequestSource = (_importName: string) =>
851
+ `const sseRequest = <
852
+ Type,
853
+ DecodingServices
854
+ >(
855
+ schema: Schema.Decoder<Type, DecodingServices>
856
+ ) =>
857
+ (
858
+ request: HttpClientRequest.HttpClientRequest
859
+ ): Stream.Stream<
860
+ { readonly event: string; readonly id: string | undefined; readonly data: Type },
861
+ HttpClientError.HttpClientError | SchemaError | Sse.Retry,
862
+ DecodingServices
863
+ > =>
864
+ HttpClient.filterStatusOk(httpClient).execute(request).pipe(
865
+ Effect.map((response) => response.stream),
866
+ Stream.unwrap,
867
+ Stream.decodeText(),
868
+ Stream.pipeThroughChannel(Sse.decodeDataSchema(schema))
869
+ )`
870
+
871
+ const binaryRequestSource =
872
+ `const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, HttpClientError.HttpClientError> =>
873
+ HttpClient.filterStatusOk(httpClient).execute(request).pipe(
874
+ Effect.map((response) => response.stream),
875
+ Stream.unwrap
876
+ )`
877
+
878
+ // Type-only mode helpers (no schema decoding)
879
+ const sseRequestSourceTs =
880
+ `const sseRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<unknown, HttpClientError.HttpClientError> =>
881
+ HttpClient.filterStatusOk(httpClient).execute(request).pipe(
882
+ Effect.map((response) => response.stream),
883
+ Stream.unwrap,
884
+ Stream.decodeText(),
885
+ Stream.splitLines,
886
+ Stream.filter((line) => line.startsWith("data: ")),
887
+ Stream.map((line) => JSON.parse(line.slice(6)))
888
+ )`
889
+
890
+ const binaryRequestSourceTs =
891
+ `const binaryRequest = (request: HttpClientRequest.HttpClientRequest): Stream.Stream<Uint8Array, HttpClientError.HttpClientError> =>
892
+ HttpClient.filterStatusOk(httpClient).execute(request).pipe(
893
+ Effect.map((response) => response.stream),
894
+ Stream.unwrap
895
+ )`
896
+
897
+ const clientErrorSource = (
898
+ name: string
899
+ ) =>
900
+ `export interface ${name}Error<Tag extends string, E> {
901
+ readonly _tag: Tag
902
+ readonly request: HttpClientRequest.HttpClientRequest
903
+ readonly response: HttpClientResponse.HttpClientResponse
904
+ readonly cause: E
905
+ }
906
+
907
+ class ${name}ErrorImpl extends Data.Error<{
908
+ _tag: string
909
+ cause: any
910
+ request: HttpClientRequest.HttpClientRequest
911
+ response: HttpClientResponse.HttpClientResponse
912
+ }> {}
913
+
914
+ export const ${name}Error = <Tag extends string, E>(
915
+ tag: Tag,
916
+ cause: E,
917
+ response: HttpClientResponse.HttpClientResponse,
918
+ ): ${name}Error<Tag, E> =>
919
+ new ${name}ErrorImpl({
920
+ _tag: tag,
921
+ cause,
922
+ response,
923
+ request: response.request,
924
+ }) as any`
925
+
926
+ const resolveConfigAccessor = (operation: ParsedOperation, rootKey: string, configKey: string): string => {
927
+ // If an operation payload is defined, then the root object must exist
928
+ if (Predicate.isNotUndefined(operation.payload)) {
929
+ return `${rootKey}.${configKey}`
930
+ }
931
+
932
+ // If operation parameters are defined and non-optional, then the root object must exist
933
+ if (Predicate.isNotUndefined(operation.params) && !operation.paramsOptional) {
934
+ return `${rootKey}.${configKey}`
935
+ }
936
+
937
+ // User-specified arguments are allowed but are not required, so the root object is optional
938
+ return `${rootKey}?.${configKey}`
939
+ }
940
+
941
+ const resolveParamsAccessor = (operation: ParsedOperation, rootKey: string, paramsKey: string): string => {
942
+ // If an operation payload is not defined and parameters are optional, then the
943
+ // root object may or may not exist and parameters must be marked as optional
944
+ if (Predicate.isUndefined(operation.payload) && operation.paramsOptional) {
945
+ return `${rootKey}?.${paramsKey}?.`
946
+ }
947
+
948
+ // If parameters are optional, they must be marked as optional
949
+ if (operation.paramsOptional) {
950
+ return `${rootKey}.${paramsKey}?.`
951
+ }
952
+
953
+ return `${rootKey}.${paramsKey}`
954
+ }