@f3liz/rescript-autogen-openapi 0.3.1

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 (72) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +111 -0
  3. package/lib/es6/src/Codegen.d.ts +28 -0
  4. package/lib/es6/src/Codegen.mjs +423 -0
  5. package/lib/es6/src/Types.d.ts +286 -0
  6. package/lib/es6/src/Types.mjs +20 -0
  7. package/lib/es6/src/bindings/Toposort.mjs +12 -0
  8. package/lib/es6/src/core/CodegenUtils.mjs +261 -0
  9. package/lib/es6/src/core/DocOverride.mjs +399 -0
  10. package/lib/es6/src/core/FileSystem.d.ts +4 -0
  11. package/lib/es6/src/core/FileSystem.mjs +78 -0
  12. package/lib/es6/src/core/IRBuilder.mjs +201 -0
  13. package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
  14. package/lib/es6/src/core/Pipeline.d.ts +6 -0
  15. package/lib/es6/src/core/Pipeline.mjs +150 -0
  16. package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
  17. package/lib/es6/src/core/Result.mjs +378 -0
  18. package/lib/es6/src/core/SchemaIR.mjs +425 -0
  19. package/lib/es6/src/core/SchemaIRParser.mjs +683 -0
  20. package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
  21. package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
  22. package/lib/es6/src/core/SpecDiffer.mjs +251 -0
  23. package/lib/es6/src/core/SpecMerger.mjs +237 -0
  24. package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +207 -0
  25. package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
  26. package/lib/es6/src/generators/EndpointGenerator.mjs +173 -0
  27. package/lib/es6/src/generators/IRToSuryGenerator.mjs +543 -0
  28. package/lib/es6/src/generators/IRToTypeGenerator.mjs +592 -0
  29. package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
  30. package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
  31. package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
  32. package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
  33. package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
  34. package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
  35. package/lib/es6/src/types/CodegenError.d.ts +66 -0
  36. package/lib/es6/src/types/CodegenError.mjs +79 -0
  37. package/lib/es6/src/types/Config.d.ts +31 -0
  38. package/lib/es6/src/types/Config.mjs +42 -0
  39. package/lib/es6/src/types/GenerationContext.mjs +47 -0
  40. package/package.json +53 -0
  41. package/rescript.json +26 -0
  42. package/src/Codegen.res +231 -0
  43. package/src/Types.res +222 -0
  44. package/src/bindings/Toposort.res +16 -0
  45. package/src/core/CodegenUtils.res +180 -0
  46. package/src/core/DocOverride.res +504 -0
  47. package/src/core/FileSystem.res +63 -0
  48. package/src/core/IRBuilder.res +66 -0
  49. package/src/core/OpenAPIParser.res +144 -0
  50. package/src/core/Pipeline.res +52 -0
  51. package/src/core/ReferenceResolver.res +41 -0
  52. package/src/core/Result.res +187 -0
  53. package/src/core/SchemaIR.res +291 -0
  54. package/src/core/SchemaIRParser.res +454 -0
  55. package/src/core/SchemaRefResolver.res +143 -0
  56. package/src/core/SchemaRegistry.res +107 -0
  57. package/src/core/SpecDiffer.res +270 -0
  58. package/src/core/SpecMerger.res +245 -0
  59. package/src/generators/ComponentSchemaGenerator.res +210 -0
  60. package/src/generators/DiffReportGenerator.res +152 -0
  61. package/src/generators/EndpointGenerator.res +176 -0
  62. package/src/generators/IRToSuryGenerator.res +386 -0
  63. package/src/generators/IRToTypeGenerator.res +423 -0
  64. package/src/generators/IRToTypeScriptGenerator.res +77 -0
  65. package/src/generators/ModuleGenerator.res +363 -0
  66. package/src/generators/SchemaCodeGenerator.res +84 -0
  67. package/src/generators/ThinWrapperGenerator.res +124 -0
  68. package/src/generators/TypeScriptDtsGenerator.res +193 -0
  69. package/src/generators/TypeScriptWrapperGenerator.res +166 -0
  70. package/src/types/CodegenError.res +85 -0
  71. package/src/types/Config.res +95 -0
  72. package/src/types/GenerationContext.res +56 -0
@@ -0,0 +1,386 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // IRToSuryGenerator.res - Generate Sury schema code from Schema IR
4
+ open Types
5
+
6
+ let addWarning = GenerationContext.addWarning
7
+
8
+ let applyConstraints = (base, min, max, toString) => {
9
+ let s1 = switch min {
10
+ | Some(v) => `${base}->S.min(${toString(v)})`
11
+ | None => base
12
+ }
13
+ switch max {
14
+ | Some(v) => `${s1}->S.max(${toString(v)})`
15
+ | None => s1
16
+ }
17
+ }
18
+
19
+ // When extractedTypeMap is provided, complex inline types reference extracted schemas instead of regenerating
20
+ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, ~extractedTypeMap: option<array<GenerationContext.extractedType>>=?, irType: SchemaIR.irType): string => {
21
+ // We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
22
+ if depth > 100 {
23
+ addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
24
+ "S.json"
25
+ } else {
26
+ let recurse = nextIrType => generateSchemaWithContext(~ctx, ~depth=depth + 1, ~extractedTypeMap?, nextIrType)
27
+
28
+ // Check if this irType was extracted — if so, reference the schema by name
29
+ let foundExtracted = switch extractedTypeMap {
30
+ | Some(extracted) =>
31
+ extracted->Array.find(({irType: extractedIr}: GenerationContext.extractedType) =>
32
+ SchemaIR.equals(extractedIr, irType)
33
+ )
34
+ | None => None
35
+ }
36
+
37
+ switch foundExtracted {
38
+ | Some({typeName}) => `${typeName}Schema`
39
+ | None =>
40
+
41
+ switch irType {
42
+ | String({constraints: c}) =>
43
+ let s = applyConstraints("S.string", c.minLength, c.maxLength, v => Int.toString(v))
44
+ switch c.pattern {
45
+ | Some(p) => `${s}->S.pattern(%re("/${CodegenUtils.escapeString(p)}/"))`
46
+ | None => s
47
+ }
48
+ | Number({constraints: c}) =>
49
+ applyConstraints("S.float", c.minimum, c.maximum, v => Float.toInt(v)->Int.toString)
50
+ | Integer({constraints: c}) =>
51
+ applyConstraints("S.int", c.minimum, c.maximum, v => Float.toInt(v)->Int.toString)
52
+ | Boolean => "S.bool"
53
+ | Null => "S.null"
54
+ | Array({items, constraints: c}) =>
55
+ applyConstraints(`S.array(${recurse(items)})`, c.minItems, c.maxItems, v => Int.toString(v))
56
+ | Object({properties, additionalProperties}) =>
57
+ if Array.length(properties) == 0 {
58
+ switch additionalProperties {
59
+ | Some(valueType) => `S.dict(${recurse(valueType)})`
60
+ | None => "S.dict(S.json)"
61
+ }
62
+ } else {
63
+ let fields =
64
+ properties
65
+ ->Array.map(((name, fieldType, isRequired)) => {
66
+ let schemaCode = recurse(fieldType)
67
+ let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
68
+ let alreadyNullable = String.startsWith(schemaCode, "S.nullableAsOption(") || switch fieldType {
69
+ | Option(_) => true
70
+ | Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
71
+ | _ => false
72
+ }
73
+ if isRequired {
74
+ ` ${camelName}: s.field("${name}", ${schemaCode}),`
75
+ } else if alreadyNullable {
76
+ ` ${camelName}: s.fieldOr("${name}", ${schemaCode}, None),`
77
+ } else {
78
+ ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
79
+ }
80
+ })
81
+ ->Array.join("\n")
82
+ `S.object(s => {\n${fields}\n })`
83
+ }
84
+ | Literal(value) =>
85
+ switch value {
86
+ | StringLiteral(s) => `S.literal("${CodegenUtils.escapeString(s)}")`
87
+ | NumberLiteral(n) => `S.literal(${Float.toString(n)})`
88
+ | BooleanLiteral(b) => `S.literal(${b ? "true" : "false"})`
89
+ | NullLiteral => "S.literal(null)"
90
+ }
91
+ | Union(types) =>
92
+ // Separate Null from non-null members (handles OpenAPI 3.1 nullable via oneOf)
93
+ let nonNullTypes = types->Array.filter(t =>
94
+ switch t {
95
+ | Null | Literal(NullLiteral) => false
96
+ | _ => true
97
+ }
98
+ )
99
+ let hasNull = Array.length(nonNullTypes) < Array.length(types)
100
+
101
+ // If the union is just [T, null], treat as nullable
102
+ if hasNull && Array.length(nonNullTypes) == 1 {
103
+ `S.nullableAsOption(${recurse(nonNullTypes->Array.getUnsafe(0))})`
104
+ } else {
105
+ let effectiveTypes = hasNull ? nonNullTypes : types
106
+
107
+ let (hasArray, hasNonArray, arrayItemType, nonArrayType) = effectiveTypes->Array.reduce(
108
+ (false, false, None, None),
109
+ ((hArr, hNonArr, arrItem, nonArr), t) =>
110
+ switch t {
111
+ | Array({items}) => (true, hNonArr, Some(items), nonArr)
112
+ | _ => (hArr, true, arrItem, Some(t))
113
+ },
114
+ )
115
+
116
+ let result = if (
117
+ hasArray &&
118
+ hasNonArray &&
119
+ Array.length(effectiveTypes) == 2 &&
120
+ SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
121
+ ) {
122
+ `S.array(${recurse(Option.getOr(arrayItemType, Unknown))})`
123
+ } else if (
124
+ effectiveTypes->Array.every(t =>
125
+ switch t {
126
+ | Literal(StringLiteral(_)) => true
127
+ | _ => false
128
+ }
129
+ ) &&
130
+ Array.length(effectiveTypes) > 0 &&
131
+ Array.length(effectiveTypes) <= 50
132
+ ) {
133
+ `S.union([${effectiveTypes->Array.map(recurse)->Array.join(", ")}])`
134
+ } else if Array.length(effectiveTypes) > 0 {
135
+ // Check if @unboxed variant is valid (same logic as type generator)
136
+ let canUnbox = {
137
+ let runtimeKinds: Dict.t<int> = Dict.make()
138
+ effectiveTypes->Array.forEach(t => {
139
+ let kind = switch t {
140
+ | Boolean | Literal(BooleanLiteral(_)) => "boolean"
141
+ | String(_) | Literal(StringLiteral(_)) => "string"
142
+ | Number(_) | Integer(_) | Literal(NumberLiteral(_)) => "number"
143
+ | Array(_) => "array"
144
+ | Object(_) | Reference(_) | Intersection(_) => "object"
145
+ | Null | Literal(NullLiteral) => "null"
146
+ | _ => "unknown"
147
+ }
148
+ let count = runtimeKinds->Dict.get(kind)->Option.getOr(0)
149
+ runtimeKinds->Dict.set(kind, count + 1)
150
+ })
151
+ Dict.valuesToArray(runtimeKinds)->Array.every(count => count <= 1)
152
+ }
153
+
154
+ if canUnbox {
155
+ // @unboxed variant with S.union + S.shape
156
+ let rawNames = effectiveTypes->Array.map(CodegenUtils.variantConstructorName)
157
+ let names = CodegenUtils.deduplicateNames(rawNames)
158
+
159
+ let branches = effectiveTypes->Array.mapWithIndex((memberType, i) => {
160
+ let constructorName = names->Array.getUnsafe(i)
161
+ switch memberType {
162
+ | Object({properties, additionalProperties}) =>
163
+ if Array.length(properties) == 0 {
164
+ switch additionalProperties {
165
+ | Some(valueType) => `S.dict(${recurse(valueType)})->S.shape(v => ${constructorName}(v))`
166
+ | None => `S.dict(S.json)->S.shape(v => ${constructorName}(v))`
167
+ }
168
+ } else {
169
+ let fields = properties->Array.map(((name, fieldType, isRequired)) => {
170
+ let schemaCode = recurse(fieldType)
171
+ let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
172
+ let alreadyNullable = String.startsWith(schemaCode, "S.nullableAsOption(") || switch fieldType {
173
+ | Option(_) => true
174
+ | Union(unionTypes) => unionTypes->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
175
+ | _ => false
176
+ }
177
+ if isRequired {
178
+ ` ${camelName}: s.field("${name}", ${schemaCode}),`
179
+ } else if alreadyNullable {
180
+ ` ${camelName}: s.fieldOr("${name}", ${schemaCode}, None),`
181
+ } else {
182
+ ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
183
+ }
184
+ })->Array.join("\n")
185
+ `S.object(s => ${constructorName}({\n${fields}\n }))`
186
+ }
187
+ | _ =>
188
+ let innerSchema = recurse(memberType)
189
+ `${innerSchema}->S.shape(v => ${constructorName}(v))`
190
+ }
191
+ })
192
+ `S.union([${branches->Array.join(", ")}])`
193
+ } else {
194
+ // Can't use @unboxed: pick last schema (matching type gen)
195
+ recurse(effectiveTypes->Array.getUnsafe(Array.length(effectiveTypes) - 1))
196
+ }
197
+ } else {
198
+ "S.json"
199
+ }
200
+
201
+ hasNull ? `S.nullableAsOption(${result})` : result
202
+ }
203
+ | Intersection(types) =>
204
+ if types->Array.every(t =>
205
+ switch t {
206
+ | Reference(_) => true
207
+ | _ => false
208
+ }
209
+ ) && Array.length(types) > 0 {
210
+ recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
211
+ } else {
212
+ // Try to merge all Object types in the intersection
213
+ let (objectProps, nonObjectTypes) = types->Array.reduce(
214
+ ([], []),
215
+ ((props, nonObj), t) =>
216
+ switch t {
217
+ | Object({properties}) => (Array.concat(props, properties), nonObj)
218
+ | _ => (props, Array.concat(nonObj, [t]))
219
+ },
220
+ )
221
+ if Array.length(objectProps) > 0 && Array.length(nonObjectTypes) == 0 {
222
+ // All objects: merge properties into single S.object
223
+ let fields =
224
+ objectProps
225
+ ->Array.map(((name, fieldType, isRequired)) => {
226
+ let schemaCode = recurse(fieldType)
227
+ let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
228
+ let alreadyNullable = String.startsWith(schemaCode, "S.nullableAsOption(") || switch fieldType {
229
+ | Option(_) => true
230
+ | Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
231
+ | _ => false
232
+ }
233
+ if isRequired {
234
+ ` ${camelName}: s.field("${name}", ${schemaCode}),`
235
+ } else if alreadyNullable {
236
+ ` ${camelName}: s.fieldOr("${name}", ${schemaCode}, None),`
237
+ } else {
238
+ ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
239
+ }
240
+ })
241
+ ->Array.join("\n")
242
+ `S.object(s => {\n${fields}\n })`
243
+ } else if Array.length(nonObjectTypes) > 0 && Array.length(objectProps) == 0 {
244
+ recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
245
+ } else {
246
+ addWarning(
247
+ ctx,
248
+ IntersectionNotFullySupported({location: ctx.path, note: "Mixed object/non-object intersection"}),
249
+ )
250
+ let fields =
251
+ objectProps
252
+ ->Array.map(((name, fieldType, isRequired)) => {
253
+ let schemaCode = recurse(fieldType)
254
+ let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
255
+ let alreadyNullable = String.startsWith(schemaCode, "S.nullableAsOption(") || switch fieldType {
256
+ | Option(_) => true
257
+ | Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
258
+ | _ => false
259
+ }
260
+ if isRequired {
261
+ ` ${camelName}: s.field("${name}", ${schemaCode}),`
262
+ } else if alreadyNullable {
263
+ ` ${camelName}: s.fieldOr("${name}", ${schemaCode}, None),`
264
+ } else {
265
+ ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
266
+ }
267
+ })
268
+ ->Array.join("\n")
269
+ `S.object(s => {\n${fields}\n })`
270
+ }
271
+ }
272
+ | Reference(ref) =>
273
+ // After IR normalization, ref may be just the schema name
274
+ let refName = if ref->String.includes("/") {
275
+ ref->String.split("/")->Array.get(ref->String.split("/")->Array.length - 1)->Option.getOr("")
276
+ } else {
277
+ ref
278
+ }
279
+
280
+ // Detect self-reference using selfRefName from context
281
+ let isSelfRef = switch ctx.selfRefName {
282
+ | Some(selfName) => refName == selfName
283
+ | None => false
284
+ }
285
+
286
+ if isSelfRef {
287
+ "schema" // Self-reference: use the recursive schema binding
288
+ } else {
289
+ let schemaPath = switch ctx.availableSchemas {
290
+ | Some(available) =>
291
+ available->Array.includes(refName)
292
+ ? `${CodegenUtils.toPascalCase(refName)}.schema`
293
+ : `ComponentSchemas.${CodegenUtils.toPascalCase(refName)}.schema`
294
+ | None =>
295
+ ReferenceResolver.refToSchemaPath(
296
+ ~insideComponentSchemas=ctx.insideComponentSchemas,
297
+ ~modulePrefix=ctx.modulePrefix,
298
+ ref,
299
+ )->Option.getOr("S.json")
300
+ }
301
+ if schemaPath == "S.json" {
302
+ addWarning(
303
+ ctx,
304
+ FallbackToJson({
305
+ reason: `Unresolved ref: ${ref}`,
306
+ context: {path: ctx.path, operation: "gen ref", schema: None},
307
+ }),
308
+ )
309
+ }
310
+ schemaPath
311
+ }
312
+ | Option(inner) => `S.nullableAsOption(${recurse(inner)})`
313
+ | Unknown => "S.json"
314
+ }
315
+ } // end switch foundExtracted
316
+ }
317
+ }
318
+
319
+ let generateSchema = (
320
+ ~depth=0,
321
+ ~path="",
322
+ ~insideComponentSchemas=false,
323
+ ~availableSchemas=?,
324
+ ~modulePrefix="",
325
+ irType,
326
+ ) => {
327
+ let ctx = GenerationContext.make(~path, ~insideComponentSchemas, ~availableSchemas?, ~modulePrefix, ())
328
+ (generateSchemaWithContext(~ctx, ~depth, irType), ctx.warnings)
329
+ }
330
+
331
+ let generateNamedSchema = (
332
+ ~namedSchema: SchemaIR.namedSchema,
333
+ ~insideComponentSchemas=false,
334
+ ~availableSchemas=?,
335
+ ~modulePrefix="",
336
+ ~extractedTypes: array<GenerationContext.extractedType>=[],
337
+ ) => {
338
+ let ctx = GenerationContext.make(
339
+ ~path=`schema.${namedSchema.name}`,
340
+ ~insideComponentSchemas,
341
+ ~availableSchemas?,
342
+ ~modulePrefix,
343
+ (),
344
+ )
345
+ let doc = switch namedSchema.description {
346
+ | Some(d) => CodegenUtils.generateDocComment(~description=d, ())
347
+ | None => ""
348
+ }
349
+ let extractedTypeMap = if Array.length(extractedTypes) > 0 { Some(extractedTypes) } else { None }
350
+ let mainSchema = generateSchemaWithContext(~ctx, ~depth=0, ~extractedTypeMap?, namedSchema.type_)
351
+
352
+ // Generate schemas for extracted auxiliary types
353
+ // Exclude the type being generated from the map to avoid self-reference
354
+ let extractedDefs = extractedTypes->Array.map(({typeName, irType}: GenerationContext.extractedType) => {
355
+ let auxCtx = GenerationContext.make(
356
+ ~path=`schema.${typeName}`,
357
+ ~insideComponentSchemas,
358
+ ~availableSchemas?,
359
+ ~modulePrefix,
360
+ (),
361
+ )
362
+ let filteredMap = extractedTypes->Array.filter(({typeName: tn}: GenerationContext.extractedType) => tn != typeName)
363
+ let auxExtractedTypeMap = if Array.length(filteredMap) > 0 { Some(filteredMap) } else { None }
364
+ let auxSchema = generateSchemaWithContext(~ctx=auxCtx, ~depth=0, ~extractedTypeMap=?auxExtractedTypeMap, irType)
365
+ `let ${typeName}Schema = ${auxSchema}`
366
+ })
367
+
368
+ let allDefs = Array.concat(extractedDefs, [`${doc}let ${namedSchema.name}Schema = ${mainSchema}`])
369
+ (
370
+ allDefs->Array.join("\n\n"),
371
+ ctx.warnings,
372
+ )
373
+ }
374
+
375
+ let generateAllSchemas = (~context: SchemaIR.schemaContext) => {
376
+ let warnings = []
377
+ let schemas =
378
+ Dict.valuesToArray(context.schemas)
379
+ ->Array.toSorted((a, b) => String.compare(a.name, b.name))
380
+ ->Array.map(s => {
381
+ let (code, w) = generateNamedSchema(~namedSchema=s)
382
+ warnings->Array.pushMany(w)
383
+ code
384
+ })
385
+ (schemas, warnings)
386
+ }