@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.
@@ -3,6 +3,19 @@ import * as JsonSchema from "effect/JsonSchema"
3
3
  import * as Rec from "effect/Record"
4
4
  import * as SchemaRepresentation from "effect/SchemaRepresentation"
5
5
 
6
+ type Source = "openapi-3.0" | "openapi-3.1"
7
+
8
+ interface GenerateOptions {
9
+ readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined
10
+ }
11
+
12
+ interface GenerateHttpApiOptions extends GenerateOptions {
13
+ readonly multipartSchemaRefs?: {
14
+ readonly singleFile: string
15
+ readonly files: string
16
+ } | undefined
17
+ }
18
+
6
19
  export function make() {
7
20
  const store: Record<string, JsonSchema.JsonSchema> = {}
8
21
 
@@ -15,80 +28,260 @@ export function make() {
15
28
  }
16
29
 
17
30
  function generate(
18
- source: "openapi-3.0" | "openapi-3.1",
31
+ source: Source,
19
32
  components: JsonSchema.Definitions,
20
33
  typeOnly: boolean,
21
- options?: {
22
- readonly onEnter?: ((js: JsonSchema.JsonSchema) => JsonSchema.JsonSchema) | undefined
34
+ options?: GenerateOptions
35
+ ) {
36
+ const generated = makeCodeDocument(source, components, options)
37
+ if (generated === undefined) {
38
+ return ""
23
39
  }
40
+
41
+ const nonRecursiveReferences = generated.codeDocument.references.nonRecursives
42
+ const recursiveReferences = Object.entries(generated.codeDocument.references.recursives)
43
+
44
+ const nonRecursives = nonRecursiveReferences.map(({ $ref, code }) =>
45
+ renderSchemaTypeAndRuntime($ref, code, typeOnly)
46
+ )
47
+
48
+ const recursiveDeclarations: Array<string> = []
49
+ const recursives: Array<string> = []
50
+
51
+ if (typeOnly) {
52
+ for (const [$ref, code] of recursiveReferences) {
53
+ recursives.push(renderSchemaTypeAndRuntime($ref, code, true))
54
+ }
55
+ } else {
56
+ const recursivelyForwardReferenced = collectForwardReferencedRecursives(
57
+ nonRecursiveReferences,
58
+ recursiveReferences
59
+ )
60
+ const recursiveInternalNames = makeRecursiveInternalNameMap(
61
+ recursivelyForwardReferenced,
62
+ [
63
+ ...nonRecursiveReferences.map(({ $ref }) => $ref),
64
+ ...recursiveReferences.map(([$ref]) => $ref),
65
+ ...generated.nameMap
66
+ ]
67
+ )
68
+
69
+ for (const [$ref, code] of recursiveReferences) {
70
+ if (recursivelyForwardReferenced.has($ref)) {
71
+ const internalName = recursiveInternalNames.get($ref)!
72
+ recursiveDeclarations.push(renderRecursiveReferenceDeclaration($ref, code, internalName))
73
+ recursives.push(`const ${internalName} = ${code.runtime}`)
74
+ continue
75
+ }
76
+
77
+ recursives.push(renderSchemaTypeAndRuntime($ref, code, false))
78
+ }
79
+ }
80
+
81
+ const codes = generated.codeDocument.codes.map((code, i) =>
82
+ renderSchemaTypeAndRuntime(generated.nameMap[i], code, typeOnly)
83
+ )
84
+
85
+ return render("recursive declarations", recursiveDeclarations) +
86
+ render("non-recursive definitions", nonRecursives) +
87
+ render("recursive definitions", recursives) +
88
+ render("schemas", codes)
89
+ }
90
+
91
+ function generateHttpApi(
92
+ source: Source,
93
+ components: JsonSchema.Definitions,
94
+ options?: GenerateHttpApiOptions
24
95
  ) {
96
+ const generated = makeCodeDocument(source, components, options)
97
+ if (generated === undefined) {
98
+ return ""
99
+ }
100
+
101
+ const nonRecursiveReferences = generated.codeDocument.references.nonRecursives
102
+ const recursiveReferences = Object.entries(generated.codeDocument.references.recursives)
103
+
104
+ const nonRecursives = nonRecursiveReferences.map(({ $ref, code }) =>
105
+ renderSchemaTypeAndRuntime($ref, code, false, options?.multipartSchemaRefs)
106
+ )
107
+
108
+ const recursivelyForwardReferenced = collectForwardReferencedRecursives(nonRecursiveReferences, recursiveReferences)
109
+ const recursiveInternalNames = makeRecursiveInternalNameMap(
110
+ recursivelyForwardReferenced,
111
+ [
112
+ ...nonRecursiveReferences.map(({ $ref }) => $ref),
113
+ ...recursiveReferences.map(([$ref]) => $ref),
114
+ ...generated.nameMap
115
+ ]
116
+ )
117
+
118
+ const recursiveDeclarations: Array<string> = []
119
+ const recursives: Array<string> = []
120
+
121
+ for (const [$ref, code] of recursiveReferences) {
122
+ if (recursivelyForwardReferenced.has($ref)) {
123
+ const internalName = recursiveInternalNames.get($ref)!
124
+ recursiveDeclarations.push(renderRecursiveReferenceDeclaration($ref, code, internalName))
125
+ recursives.push(`const ${internalName} = ${code.runtime}`)
126
+ continue
127
+ }
128
+
129
+ recursives.push(renderSchemaTypeAndRuntime($ref, code, false, options?.multipartSchemaRefs))
130
+ }
131
+
132
+ const codes = generated.codeDocument.codes.map((code, i) =>
133
+ renderSchemaTypeAndRuntime(generated.nameMap[i], code, false, options?.multipartSchemaRefs)
134
+ )
135
+
136
+ return render("recursive declarations", recursiveDeclarations) +
137
+ render("non-recursive definitions", nonRecursives) +
138
+ render("recursive definitions", recursives) +
139
+ render("schemas", codes)
140
+ }
141
+
142
+ function makeCodeDocument(
143
+ source: Source,
144
+ components: JsonSchema.Definitions,
145
+ options?: GenerateOptions
146
+ ): {
147
+ readonly nameMap: Array<string>
148
+ readonly codeDocument: SchemaRepresentation.CodeDocument
149
+ } | undefined {
25
150
  const nameMap: Array<string> = []
26
151
  const schemas: Array<JsonSchema.JsonSchema> = []
27
152
 
28
153
  const definitions: JsonSchema.Definitions = Rec.map(
29
154
  components,
30
- (js) => fromSchemaOpenApi(js).schema
155
+ (js) => fromSchemaOpenApi(source, js).schema
31
156
  )
32
157
 
33
158
  for (const [name, js] of Object.entries(store)) {
34
159
  nameMap.push(name)
35
- schemas.push(fromSchemaOpenApi(js).schema)
160
+ schemas.push(fromSchemaOpenApi(source, js).schema)
161
+ }
162
+
163
+ if (!Arr.isArrayNonEmpty(schemas)) {
164
+ return
36
165
  }
37
166
 
38
- if (Arr.isArrayNonEmpty(schemas)) {
39
- const multiDocument: SchemaRepresentation.MultiDocument = SchemaRepresentation.fromJsonSchemaMultiDocument({
40
- dialect: "draft-2020-12",
41
- schemas,
42
- definitions
43
- }, {
44
- onEnter(js) {
45
- const out = { ...js }
46
- if (out.type === "object" && out.additionalProperties === undefined) {
47
- out.additionalProperties = false
48
- }
49
- return options?.onEnter?.(out) ?? out
167
+ const multiDocument: SchemaRepresentation.MultiDocument = SchemaRepresentation.fromJsonSchemaMultiDocument({
168
+ dialect: "draft-2020-12",
169
+ schemas,
170
+ definitions
171
+ }, {
172
+ onEnter(js) {
173
+ const out = { ...js }
174
+ if (out.type === "object" && out.additionalProperties === undefined) {
175
+ out.additionalProperties = false
50
176
  }
51
- })
177
+ return options?.onEnter?.(out) ?? out
178
+ }
179
+ })
52
180
 
53
- const codeDocument = SchemaRepresentation.toCodeDocument(multiDocument)
181
+ return {
182
+ nameMap,
183
+ codeDocument: SchemaRepresentation.toCodeDocument(multiDocument)
184
+ }
185
+ }
54
186
 
55
- const nonRecursives = codeDocument.references.nonRecursives.map(({ $ref, code }) => renderSchema($ref, code))
56
- const recursives = Object.entries(codeDocument.references.recursives).map(([$ref, code]) =>
57
- renderSchema($ref, code)
58
- )
59
- const codes = codeDocument.codes.map((code, i) => renderSchema(nameMap[i], code))
187
+ return { addSchema, generate, generateHttpApi } as const
188
+ }
60
189
 
61
- const s = render("non-recursive definitions", nonRecursives) +
62
- render("recursive definitions", recursives) +
63
- render("schemas", codes)
190
+ function fromSchemaOpenApi(source: Source, jsonSchema: JsonSchema.JsonSchema) {
191
+ switch (source) {
192
+ case "openapi-3.1":
193
+ return JsonSchema.fromSchemaOpenApi3_1(jsonSchema)
194
+ case "openapi-3.0":
195
+ return JsonSchema.fromSchemaOpenApi3_0(jsonSchema)
196
+ }
197
+ }
64
198
 
65
- return s
66
- } else {
67
- return ""
199
+ function renderSchemaTypeAndRuntime(
200
+ $ref: string,
201
+ code: SchemaRepresentation.Code,
202
+ typeOnly: boolean,
203
+ multipartSchemaRefs?: {
204
+ readonly singleFile: string
205
+ readonly files: string
206
+ }
207
+ ) {
208
+ if (!typeOnly && multipartSchemaRefs !== undefined) {
209
+ if ($ref === multipartSchemaRefs.singleFile) {
210
+ return [
211
+ `export type ${$ref} = Multipart.PersistedFile`,
212
+ `export const ${$ref} = Multipart.SingleFileSchema`
213
+ ].join("\n")
68
214
  }
69
-
70
- function fromSchemaOpenApi(jsonSchema: JsonSchema.JsonSchema) {
71
- switch (source) {
72
- case "openapi-3.1":
73
- return JsonSchema.fromSchemaOpenApi3_1(jsonSchema)
74
- case "openapi-3.0":
75
- return JsonSchema.fromSchemaOpenApi3_0(jsonSchema)
76
- }
215
+ if ($ref === multipartSchemaRefs.files) {
216
+ return [
217
+ `export type ${$ref} = ReadonlyArray<Multipart.PersistedFile>`,
218
+ `export const ${$ref} = Multipart.FilesSchema`
219
+ ].join("\n")
77
220
  }
221
+ }
222
+
223
+ const strings = [`export type ${$ref} = ${code.Type}`]
224
+ if (!typeOnly) {
225
+ strings.push(`export const ${$ref} = ${code.runtime}`)
226
+ }
227
+ return strings.join("\n")
228
+ }
229
+
230
+ function renderRecursiveReferenceDeclaration(
231
+ $ref: string,
232
+ code: SchemaRepresentation.Code,
233
+ internalName: string
234
+ ): string {
235
+ return [
236
+ `export type ${$ref} = ${code.Type}`,
237
+ `export const ${$ref} = Schema.suspend((): Schema.Codec<${$ref}> => ${internalName})`
238
+ ].join("\n")
239
+ }
240
+
241
+ function render(title: string, as: ReadonlyArray<string>) {
242
+ if (as.length === 0) return ""
243
+ return "// " + title + "\n" + as.join("\n") + "\n"
244
+ }
245
+
246
+ const tokenPattern = /[A-Za-z_$][A-Za-z0-9_$]*/g
247
+
248
+ function collectForwardReferencedRecursives(
249
+ nonRecursives: ReadonlyArray<{
250
+ readonly $ref: string
251
+ readonly code: SchemaRepresentation.Code
252
+ }>,
253
+ recursives: ReadonlyArray<readonly [string, SchemaRepresentation.Code]>
254
+ ): Set<string> {
255
+ const recursiveNames = new Set(recursives.map(([name]) => name))
256
+ const referenced = new Set<string>()
78
257
 
79
- function renderSchema($ref: string, code: SchemaRepresentation.Code) {
80
- const strings = [`export type ${$ref} = ${code.Type}`]
81
- if (!typeOnly) {
82
- strings.push(`export const ${$ref} = ${code.runtime}`)
258
+ for (const { code } of nonRecursives) {
259
+ for (const token of code.runtime.matchAll(tokenPattern)) {
260
+ const identifier = token[0]
261
+ if (recursiveNames.has(identifier)) {
262
+ referenced.add(identifier)
83
263
  }
84
- return strings.join("\n")
85
264
  }
265
+ }
266
+
267
+ return referenced
268
+ }
269
+
270
+ function makeRecursiveInternalNameMap(
271
+ recursiveNames: ReadonlySet<string>,
272
+ existingNames: ReadonlyArray<string>
273
+ ): Map<string, string> {
274
+ const usedNames = new Set(existingNames)
275
+ const internalNames = new Map<string, string>()
86
276
 
87
- function render(title: string, as: ReadonlyArray<string>) {
88
- if (as.length === 0) return ""
89
- return "// " + title + "\n" + as.join("\n") + "\n"
277
+ for (const name of recursiveNames) {
278
+ let candidate = `__recursive_${name}`
279
+ while (usedNames.has(candidate)) {
280
+ candidate = `_${candidate}`
90
281
  }
282
+ usedNames.add(candidate)
283
+ internalNames.set(name, candidate)
91
284
  }
92
285
 
93
- return { addSchema, generate } as const
286
+ return internalNames
94
287
  }