@f3liz/rescript-autogen-openapi 0.2.0 → 0.3.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.
Potentially problematic release.
This version of @f3liz/rescript-autogen-openapi might be problematic. Click here for more details.
- package/lib/es6/src/bindings/Toposort.mjs +12 -0
- package/lib/es6/src/core/CodegenUtils.mjs +75 -0
- package/lib/es6/src/core/SchemaIR.mjs +72 -2
- package/lib/es6/src/core/SchemaIRParser.mjs +244 -51
- package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +118 -36
- package/lib/es6/src/generators/EndpointGenerator.mjs +4 -3
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +271 -34
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +491 -285
- package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +1 -1
- package/lib/es6/src/generators/ModuleGenerator.mjs +1 -1
- package/lib/es6/src/generators/SchemaCodeGenerator.mjs +1 -1
- package/lib/es6/src/types/GenerationContext.mjs +25 -2
- package/package.json +3 -2
- package/src/bindings/Toposort.res +16 -0
- package/src/core/CodegenUtils.res +50 -0
- package/src/core/SchemaIR.res +33 -0
- package/src/core/SchemaIRParser.res +96 -2
- package/src/generators/ComponentSchemaGenerator.res +133 -50
- package/src/generators/EndpointGenerator.res +7 -3
- package/src/generators/IRToSuryGenerator.res +175 -40
- package/src/generators/IRToTypeGenerator.res +212 -63
- package/src/generators/IRToTypeScriptGenerator.res +6 -1
- package/src/generators/ModuleGenerator.res +2 -1
- package/src/generators/SchemaCodeGenerator.res +2 -1
- package/src/types/GenerationContext.res +34 -1
|
@@ -16,13 +16,27 @@ let applyConstraints = (base, min, max, toString) => {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
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 => {
|
|
20
21
|
// We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
|
|
21
22
|
if depth > 100 {
|
|
22
23
|
addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
|
|
23
24
|
"S.json"
|
|
24
25
|
} else {
|
|
25
|
-
let recurse = nextIrType => generateSchemaWithContext(~ctx, ~depth=depth + 1, nextIrType)
|
|
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 =>
|
|
26
40
|
|
|
27
41
|
switch irType {
|
|
28
42
|
| String({constraints: c}) =>
|
|
@@ -43,7 +57,7 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
43
57
|
if Array.length(properties) == 0 {
|
|
44
58
|
switch additionalProperties {
|
|
45
59
|
| Some(valueType) => `S.dict(${recurse(valueType)})`
|
|
46
|
-
| None => "S.json"
|
|
60
|
+
| None => "S.dict(S.json)"
|
|
47
61
|
}
|
|
48
62
|
} else {
|
|
49
63
|
let fields =
|
|
@@ -51,9 +65,18 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
51
65
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
52
66
|
let schemaCode = recurse(fieldType)
|
|
53
67
|
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
80
|
})
|
|
58
81
|
->Array.join("\n")
|
|
59
82
|
`S.object(s => {\n${fields}\n })`
|
|
@@ -109,8 +132,68 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
109
132
|
) {
|
|
110
133
|
`S.union([${effectiveTypes->Array.map(recurse)->Array.join(", ")}])`
|
|
111
134
|
} else if Array.length(effectiveTypes) > 0 {
|
|
112
|
-
//
|
|
113
|
-
|
|
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
|
+
}
|
|
114
197
|
} else {
|
|
115
198
|
"S.json"
|
|
116
199
|
}
|
|
@@ -142,9 +225,18 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
142
225
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
143
226
|
let schemaCode = recurse(fieldType)
|
|
144
227
|
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|
|
148
240
|
})
|
|
149
241
|
->Array.join("\n")
|
|
150
242
|
`S.object(s => {\n${fields}\n })`
|
|
@@ -160,45 +252,67 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
160
252
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
161
253
|
let schemaCode = recurse(fieldType)
|
|
162
254
|
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
267
|
})
|
|
167
268
|
->Array.join("\n")
|
|
168
269
|
`S.object(s => {\n${fields}\n })`
|
|
169
270
|
}
|
|
170
271
|
}
|
|
171
272
|
| Reference(ref) =>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
->Array.get(ref->String.split("/")->Array.length - 1)
|
|
178
|
-
->Option.getOr("")
|
|
179
|
-
available->Array.includes(name)
|
|
180
|
-
? `${CodegenUtils.toPascalCase(name)}.schema`
|
|
181
|
-
: `ComponentSchemas.${CodegenUtils.toPascalCase(name)}.schema`
|
|
182
|
-
| None =>
|
|
183
|
-
ReferenceResolver.refToSchemaPath(
|
|
184
|
-
~insideComponentSchemas=ctx.insideComponentSchemas,
|
|
185
|
-
~modulePrefix=ctx.modulePrefix,
|
|
186
|
-
ref,
|
|
187
|
-
)->Option.getOr("S.json")
|
|
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
|
|
188
278
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
311
|
}
|
|
198
|
-
schemaPath
|
|
199
312
|
| Option(inner) => `S.nullableAsOption(${recurse(inner)})`
|
|
200
313
|
| Unknown => "S.json"
|
|
201
314
|
}
|
|
315
|
+
} // end switch foundExtracted
|
|
202
316
|
}
|
|
203
317
|
}
|
|
204
318
|
|
|
@@ -219,6 +333,7 @@ let generateNamedSchema = (
|
|
|
219
333
|
~insideComponentSchemas=false,
|
|
220
334
|
~availableSchemas=?,
|
|
221
335
|
~modulePrefix="",
|
|
336
|
+
~extractedTypes: array<GenerationContext.extractedType>=[],
|
|
222
337
|
) => {
|
|
223
338
|
let ctx = GenerationContext.make(
|
|
224
339
|
~path=`schema.${namedSchema.name}`,
|
|
@@ -231,8 +346,28 @@ let generateNamedSchema = (
|
|
|
231
346
|
| Some(d) => CodegenUtils.generateDocComment(~description=d, ())
|
|
232
347
|
| None => ""
|
|
233
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}`])
|
|
234
369
|
(
|
|
235
|
-
|
|
370
|
+
allDefs->Array.join("\n\n"),
|
|
236
371
|
ctx.warnings,
|
|
237
372
|
)
|
|
238
373
|
}
|