@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,309 @@
1
+ import * as Effect from "effect/Effect"
2
+ import type * as JsonSchema from "effect/JsonSchema"
3
+ import * as Layer from "effect/Layer"
4
+ import * as Predicate from "effect/Predicate"
5
+ import * as ServiceMap from "effect/ServiceMap"
6
+ import * as String from "effect/String"
7
+ import type { OpenAPISpec, OpenAPISpecMethodName, OpenAPISpecPathItem } from "effect/unstable/httpapi/OpenApi"
8
+ import SwaggerToOpenApi from "swagger2openapi"
9
+ import * as JsonSchemaGenerator from "./JsonSchemaGenerator.ts"
10
+ import * as OpenApiTransformer from "./OpenApiTransformer.ts"
11
+ import * as ParsedOperation from "./ParsedOperation.ts"
12
+ import * as Utils from "./Utils.ts"
13
+
14
+ export class OpenApiGenerator extends ServiceMap.Service<
15
+ OpenApiGenerator,
16
+ { readonly generate: (spec: OpenAPISpec, options: OpenApiGenerateOptions) => Effect.Effect<string> }
17
+ >()("OpenApiGenerator") {}
18
+
19
+ export interface OpenApiGenerateOptions {
20
+ /**
21
+ * The name to give to the generated client.
22
+ */
23
+ readonly name: string
24
+ /**
25
+ * When `true`, will **only** generate types based on the provided OpenApi
26
+ * specification (without corresponding schemas).
27
+ */
28
+ readonly typeOnly: boolean
29
+ /**
30
+ * Hook to transform each JSON Schema node before processing.
31
+ */
32
+ readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined
33
+ }
34
+
35
+ const methodNames: ReadonlyArray<OpenAPISpecMethodName> = [
36
+ "get",
37
+ "put",
38
+ "post",
39
+ "delete",
40
+ "options",
41
+ "head",
42
+ "patch",
43
+ "trace"
44
+ ]
45
+
46
+ export const make = Effect.gen(function*() {
47
+ const generate = Effect.fn(
48
+ function*(spec: OpenAPISpec, options: OpenApiGenerateOptions) {
49
+ const generator = JsonSchemaGenerator.make()
50
+ const openApiTransformer = yield* OpenApiTransformer.OpenApiTransformer
51
+
52
+ // If we receive a Swagger 2.0 spec, convert it to an OpenApi 3.0 spec
53
+ if (isSwaggerSpec(spec)) {
54
+ spec = yield* convertSwaggerSpec(spec)
55
+ }
56
+
57
+ function resolveRef(ref: string) {
58
+ const parts = ref.split("/").slice(1)
59
+ let current: any = spec
60
+ for (const part of parts) {
61
+ current = current[part]
62
+ }
63
+ return current
64
+ }
65
+
66
+ const operations: Array<ParsedOperation.ParsedOperation> = []
67
+
68
+ function handlePath(path: string, methods: OpenAPISpecPathItem): void {
69
+ for (const method of methodNames) {
70
+ const operation = methods[method]
71
+
72
+ if (Predicate.isUndefined(operation)) {
73
+ continue
74
+ }
75
+
76
+ const id = operation.operationId
77
+ ? Utils.camelize(operation.operationId)
78
+ : `${method.toUpperCase()}${path}`
79
+
80
+ const description = Utils.nonEmptyString(operation.description) ?? Utils.nonEmptyString(operation.summary)
81
+
82
+ const { pathIds, pathTemplate } = processPath(path)
83
+
84
+ const op = ParsedOperation.makeDeepMutable({
85
+ id,
86
+ method,
87
+ description,
88
+ pathIds,
89
+ pathTemplate
90
+ })
91
+
92
+ const schemaId = Utils.identifier(operation.operationId ?? path)
93
+
94
+ const validParameters = operation.parameters?.filter((param) => {
95
+ return param.in !== "path" && param.in !== "cookie"
96
+ }) ?? []
97
+
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
+ }
105
+
106
+ for (let parameter of validParameters) {
107
+ if ("$ref" in parameter) {
108
+ parameter = resolveRef(parameter.$ref as string)
109
+ }
110
+
111
+ if (parameter.in === "path") {
112
+ continue
113
+ }
114
+
115
+ const paramSchema = parameter.schema
116
+ const added: Array<string> = []
117
+ if ("properties" in paramSchema && Predicate.isObject(paramSchema.properties)) {
118
+ const required = "required" in paramSchema
119
+ ? paramSchema.required as Array<string>
120
+ : []
121
+
122
+ for (const [name, propSchema] of Object.entries(paramSchema.properties)) {
123
+ const adjustedName = `${parameter.name}[${name}]`
124
+ schema.properties[adjustedName] = propSchema
125
+ if (required.includes(name)) {
126
+ schema.required.push(adjustedName)
127
+ }
128
+ added.push(adjustedName)
129
+ }
130
+ } else {
131
+ schema.properties[parameter.name] = parameter.schema
132
+ if (parameter.required) {
133
+ schema.required.push(parameter.name)
134
+ }
135
+ added.push(parameter.name)
136
+ }
137
+
138
+ if (parameter.in === "query") {
139
+ Utils.spreadElementsInto(added, op.urlParams)
140
+ } else if (parameter.in === "header") {
141
+ Utils.spreadElementsInto(added, op.headers)
142
+ } else if (parameter.in === "cookie") {
143
+ Utils.spreadElementsInto(added, op.cookies)
144
+ }
145
+ }
146
+
147
+ op.params = generator.addSchema(
148
+ `${schemaId}Params`,
149
+ schema
150
+ )
151
+
152
+ op.paramsOptional = !schema.required || schema.required.length === 0
153
+ }
154
+
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
+ }
161
+
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
168
+ }
169
+
170
+ let defaultSchema: string | undefined
171
+ for (const entry of Object.entries(operation.responses ?? {})) {
172
+ const status = entry[0]
173
+ let response = entry[1]
174
+
175
+ while ("$ref" in response) {
176
+ response = resolveRef(response.$ref as string)
177
+ }
178
+
179
+ if (Predicate.isNotUndefined(response.content?.["application/json"]?.schema)) {
180
+ const schemaName = generator.addSchema(
181
+ `${schemaId}${status}`,
182
+ response.content["application/json"].schema
183
+ )
184
+
185
+ if (status === "default") {
186
+ defaultSchema = schemaName
187
+ continue
188
+ }
189
+
190
+ const statusLower = status.toLowerCase()
191
+ const statusMajorNumber = Number(status[0])
192
+ if (Number.isNaN(statusMajorNumber)) {
193
+ continue
194
+ }
195
+ if (statusMajorNumber < 4) {
196
+ op.successSchemas.set(statusLower, schemaName)
197
+ } else {
198
+ op.errorSchemas.set(statusLower, schemaName)
199
+ }
200
+ }
201
+
202
+ // Handle SSE streaming responses (text/event-stream)
203
+ if (
204
+ Predicate.isUndefined(op.sseSchema) &&
205
+ Predicate.isNotUndefined(response.content?.["text/event-stream"]?.schema)
206
+ ) {
207
+ const statusMajorNumber = Number(status[0])
208
+ if (!Number.isNaN(statusMajorNumber) && statusMajorNumber < 4) {
209
+ op.sseSchema = generator.addSchema(
210
+ `${schemaId}${status}Sse`,
211
+ response.content["text/event-stream"].schema
212
+ )
213
+ }
214
+ }
215
+
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
+ }
222
+ }
223
+
224
+ if (Predicate.isUndefined(response.content)) {
225
+ if (status !== "default") {
226
+ op.voidSchemas.add(status.toLowerCase())
227
+ }
228
+ }
229
+ }
230
+
231
+ if (op.successSchemas.size === 0 && Predicate.isNotUndefined(defaultSchema)) {
232
+ op.successSchemas.set("2xx", defaultSchema)
233
+ }
234
+
235
+ operations.push(op)
236
+ }
237
+ }
238
+
239
+ for (const [path, methods] of Object.entries(spec.paths)) {
240
+ handlePath(path, methods)
241
+ }
242
+
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
+ })
249
+
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
+ )
257
+ },
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
+ )
266
+ )
267
+
268
+ return { generate } as const
269
+ })
270
+
271
+ function getDialect(spec: OpenAPISpec): "openapi-3.0" | "openapi-3.1" {
272
+ return spec.openapi.trim().startsWith("3.0") ? "openapi-3.0" : "openapi-3.1"
273
+ }
274
+
275
+ export const layerTransformerSchema: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
276
+
277
+ export const layerTransformerTs: Layer.Layer<OpenApiGenerator> = Layer.effect(OpenApiGenerator, make)
278
+
279
+ const isSwaggerSpec = (spec: OpenAPISpec) => "swagger" in spec
280
+
281
+ const convertSwaggerSpec = Effect.fn((spec: OpenAPISpec) =>
282
+ Effect.callback<OpenAPISpec>((resume) => {
283
+ SwaggerToOpenApi.convertObj(
284
+ spec as any,
285
+ { laxDefaults: true, laxurls: true, patch: true, warnOnly: true },
286
+ (err, result) => {
287
+ if (err) {
288
+ resume(Effect.die(err))
289
+ } else {
290
+ resume(Effect.succeed(result.openapi as any))
291
+ }
292
+ }
293
+ )
294
+ }).pipe(Effect.withSpan("OpenApi.convertSwaggerSpec"))
295
+ )
296
+
297
+ const processPath = (path: string): {
298
+ readonly pathIds: Array<string>
299
+ readonly pathTemplate: string
300
+ } => {
301
+ const pathIds: Array<string> = []
302
+ path = path.replace(/{([^}]+)}/g, (_, name) => {
303
+ const id = Utils.camelize(name)
304
+ pathIds.push(id)
305
+ return "${" + id + "}"
306
+ })
307
+ const pathTemplate = "`" + path + "`"
308
+ return { pathIds, pathTemplate } as const
309
+ }