@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.
- package/LICENSE +373 -0
- package/README.md +111 -0
- package/lib/es6/src/Codegen.d.ts +28 -0
- package/lib/es6/src/Codegen.mjs +423 -0
- package/lib/es6/src/Types.d.ts +286 -0
- package/lib/es6/src/Types.mjs +20 -0
- package/lib/es6/src/bindings/Toposort.mjs +12 -0
- package/lib/es6/src/core/CodegenUtils.mjs +261 -0
- package/lib/es6/src/core/DocOverride.mjs +399 -0
- package/lib/es6/src/core/FileSystem.d.ts +4 -0
- package/lib/es6/src/core/FileSystem.mjs +78 -0
- package/lib/es6/src/core/IRBuilder.mjs +201 -0
- package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
- package/lib/es6/src/core/Pipeline.d.ts +6 -0
- package/lib/es6/src/core/Pipeline.mjs +150 -0
- package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
- package/lib/es6/src/core/Result.mjs +378 -0
- package/lib/es6/src/core/SchemaIR.mjs +425 -0
- package/lib/es6/src/core/SchemaIRParser.mjs +683 -0
- package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
- package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
- package/lib/es6/src/core/SpecDiffer.mjs +251 -0
- package/lib/es6/src/core/SpecMerger.mjs +237 -0
- package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +207 -0
- package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
- package/lib/es6/src/generators/EndpointGenerator.mjs +173 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +543 -0
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +592 -0
- package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
- package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
- package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
- package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
- package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
- package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
- package/lib/es6/src/types/CodegenError.d.ts +66 -0
- package/lib/es6/src/types/CodegenError.mjs +79 -0
- package/lib/es6/src/types/Config.d.ts +31 -0
- package/lib/es6/src/types/Config.mjs +42 -0
- package/lib/es6/src/types/GenerationContext.mjs +47 -0
- package/package.json +53 -0
- package/rescript.json +26 -0
- package/src/Codegen.res +231 -0
- package/src/Types.res +222 -0
- package/src/bindings/Toposort.res +16 -0
- package/src/core/CodegenUtils.res +180 -0
- package/src/core/DocOverride.res +504 -0
- package/src/core/FileSystem.res +63 -0
- package/src/core/IRBuilder.res +66 -0
- package/src/core/OpenAPIParser.res +144 -0
- package/src/core/Pipeline.res +52 -0
- package/src/core/ReferenceResolver.res +41 -0
- package/src/core/Result.res +187 -0
- package/src/core/SchemaIR.res +291 -0
- package/src/core/SchemaIRParser.res +454 -0
- package/src/core/SchemaRefResolver.res +143 -0
- package/src/core/SchemaRegistry.res +107 -0
- package/src/core/SpecDiffer.res +270 -0
- package/src/core/SpecMerger.res +245 -0
- package/src/generators/ComponentSchemaGenerator.res +210 -0
- package/src/generators/DiffReportGenerator.res +152 -0
- package/src/generators/EndpointGenerator.res +176 -0
- package/src/generators/IRToSuryGenerator.res +386 -0
- package/src/generators/IRToTypeGenerator.res +423 -0
- package/src/generators/IRToTypeScriptGenerator.res +77 -0
- package/src/generators/ModuleGenerator.res +363 -0
- package/src/generators/SchemaCodeGenerator.res +84 -0
- package/src/generators/ThinWrapperGenerator.res +124 -0
- package/src/generators/TypeScriptDtsGenerator.res +193 -0
- package/src/generators/TypeScriptWrapperGenerator.res +166 -0
- package/src/types/CodegenError.res +85 -0
- package/src/types/Config.res +95 -0
- package/src/types/GenerationContext.res +56 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// IRToTypeGenerator.res - Generate ReScript types from Schema IR
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
let addWarning = GenerationContext.addWarning
|
|
7
|
+
|
|
8
|
+
// `inline` tracks whether the type appears inside a type constructor (array<_>, option<_>, etc.)
|
|
9
|
+
// where ReScript forbids inline record declarations and variant definitions.
|
|
10
|
+
// When a complex type is encountered inline, it's extracted as a separate named type.
|
|
11
|
+
let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, ~inline=false, irType: SchemaIR.irType): string => {
|
|
12
|
+
// We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
|
|
13
|
+
if depth > 100 {
|
|
14
|
+
addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
|
|
15
|
+
"JSON.t"
|
|
16
|
+
} else {
|
|
17
|
+
// Inside type constructors, records/variants can't appear; recurse as inline
|
|
18
|
+
let recurseInline = nextIrType => generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline=true, nextIrType)
|
|
19
|
+
|
|
20
|
+
switch irType {
|
|
21
|
+
| String(_) => "string"
|
|
22
|
+
| Number(_) => "float"
|
|
23
|
+
| Integer(_) => "int"
|
|
24
|
+
| Boolean => "bool"
|
|
25
|
+
| Null => "unit"
|
|
26
|
+
| Array({items}) => `array<${recurseInline(items)}>`
|
|
27
|
+
| Object({properties, additionalProperties}) =>
|
|
28
|
+
if Array.length(properties) == 0 {
|
|
29
|
+
switch additionalProperties {
|
|
30
|
+
| Some(valueType) => `dict<${recurseInline(valueType)}>`
|
|
31
|
+
| None => "dict<JSON.t>"
|
|
32
|
+
}
|
|
33
|
+
} else if inline {
|
|
34
|
+
// Extract inline record as a separate named type
|
|
35
|
+
let baseName = ctx.path->String.split(".")->Array.get(ctx.path->String.split(".")->Array.length - 1)->Option.getOr("item")
|
|
36
|
+
let typeName = GenerationContext.extractType(ctx, ~baseName, irType)
|
|
37
|
+
typeName
|
|
38
|
+
} else {
|
|
39
|
+
let fields =
|
|
40
|
+
properties
|
|
41
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
42
|
+
let typeCode = recurseInline(fieldType)
|
|
43
|
+
// Avoid double-option: check both generated string and IR type for nullability
|
|
44
|
+
let alreadyNullable = String.startsWith(typeCode, "option<") || switch fieldType {
|
|
45
|
+
| Option(_) => true
|
|
46
|
+
| Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
|
|
47
|
+
| _ => false
|
|
48
|
+
}
|
|
49
|
+
let finalType = isRequired || alreadyNullable ? typeCode : `option<${typeCode}>`
|
|
50
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
51
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
52
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
53
|
+
` ${aliasAnnotation}${escapedName}: ${finalType},`
|
|
54
|
+
})
|
|
55
|
+
->Array.join("\n")
|
|
56
|
+
`{\n${fields}\n}`
|
|
57
|
+
}
|
|
58
|
+
| Literal(value) =>
|
|
59
|
+
switch value {
|
|
60
|
+
| StringLiteral(_) => "string"
|
|
61
|
+
| NumberLiteral(_) => "float"
|
|
62
|
+
| BooleanLiteral(_) => "bool"
|
|
63
|
+
| NullLiteral => "unit"
|
|
64
|
+
}
|
|
65
|
+
| Union(types) =>
|
|
66
|
+
// Separate Null from non-null members (handles OpenAPI 3.1 nullable via oneOf)
|
|
67
|
+
let nonNullTypes = types->Array.filter(t =>
|
|
68
|
+
switch t {
|
|
69
|
+
| Null | Literal(NullLiteral) => false
|
|
70
|
+
| _ => true
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
let hasNull = Array.length(nonNullTypes) < Array.length(types)
|
|
74
|
+
|
|
75
|
+
// If the union is just [T, null], treat as option<T>
|
|
76
|
+
if hasNull && Array.length(nonNullTypes) == 1 {
|
|
77
|
+
let inner = generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline=true, nonNullTypes->Array.getUnsafe(0))
|
|
78
|
+
`option<${inner}>`
|
|
79
|
+
} else {
|
|
80
|
+
// Work with the non-null types (re-wrap in option at the end if hasNull)
|
|
81
|
+
let effectiveTypes = hasNull ? nonNullTypes : types
|
|
82
|
+
|
|
83
|
+
// Attempt to simplify common union patterns
|
|
84
|
+
let (hasArray, hasNonArray, arrayItemType, nonArrayType) = effectiveTypes->Array.reduce(
|
|
85
|
+
(false, false, None, None),
|
|
86
|
+
((hArr, hNonArr, arrItem, nonArr), t) =>
|
|
87
|
+
switch t {
|
|
88
|
+
| Array({items}) => (true, hNonArr, Some(items), nonArr)
|
|
89
|
+
| _ => (hArr, true, arrItem, Some(t))
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
let result = if (
|
|
94
|
+
hasArray &&
|
|
95
|
+
hasNonArray &&
|
|
96
|
+
Array.length(effectiveTypes) == 2 &&
|
|
97
|
+
SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
|
|
98
|
+
) {
|
|
99
|
+
`array<${recurseInline(Option.getOr(arrayItemType, Unknown))}>`
|
|
100
|
+
} else if (
|
|
101
|
+
effectiveTypes->Array.every(t =>
|
|
102
|
+
switch t {
|
|
103
|
+
| Literal(StringLiteral(_)) => true
|
|
104
|
+
| _ => false
|
|
105
|
+
}
|
|
106
|
+
) &&
|
|
107
|
+
Array.length(effectiveTypes) > 0 &&
|
|
108
|
+
Array.length(effectiveTypes) <= 50
|
|
109
|
+
) {
|
|
110
|
+
// Polymorphic variants: valid inline
|
|
111
|
+
let variants =
|
|
112
|
+
effectiveTypes
|
|
113
|
+
->Array.map(t =>
|
|
114
|
+
switch t {
|
|
115
|
+
| Literal(StringLiteral(s)) => `#${CodegenUtils.toPascalCase(s)}`
|
|
116
|
+
| _ => "#Unknown"
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
->Array.join(" | ")
|
|
120
|
+
`[${variants}]`
|
|
121
|
+
} else if Array.length(effectiveTypes) > 0 {
|
|
122
|
+
// Check if @unboxed variant is valid: each member must have a distinct runtime representation
|
|
123
|
+
let canUnbox = {
|
|
124
|
+
let runtimeKinds: Dict.t<int> = Dict.make()
|
|
125
|
+
effectiveTypes->Array.forEach(t => {
|
|
126
|
+
let kind = switch t {
|
|
127
|
+
| Boolean | Literal(BooleanLiteral(_)) => "boolean"
|
|
128
|
+
| String(_) | Literal(StringLiteral(_)) => "string"
|
|
129
|
+
| Number(_) | Integer(_) | Literal(NumberLiteral(_)) => "number"
|
|
130
|
+
| Array(_) => "array"
|
|
131
|
+
| Object(_) | Reference(_) | Intersection(_) => "object"
|
|
132
|
+
| Null | Literal(NullLiteral) => "null"
|
|
133
|
+
| _ => "unknown"
|
|
134
|
+
}
|
|
135
|
+
let count = runtimeKinds->Dict.get(kind)->Option.getOr(0)
|
|
136
|
+
runtimeKinds->Dict.set(kind, count + 1)
|
|
137
|
+
})
|
|
138
|
+
// Valid if no kind appears more than once
|
|
139
|
+
Dict.valuesToArray(runtimeKinds)->Array.every(count => count <= 1)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if canUnbox {
|
|
143
|
+
// Safe to use @unboxed variant
|
|
144
|
+
let extractIR = if hasNull {
|
|
145
|
+
SchemaIR.Union(effectiveTypes)
|
|
146
|
+
} else {
|
|
147
|
+
irType
|
|
148
|
+
}
|
|
149
|
+
let baseName = ctx.path->String.split(".")->Array.get(ctx.path->String.split(".")->Array.length - 1)->Option.getOr("union")
|
|
150
|
+
let typeName = GenerationContext.extractType(ctx, ~baseName, ~isUnboxed=true, extractIR)
|
|
151
|
+
typeName
|
|
152
|
+
} else {
|
|
153
|
+
// Can't use @unboxed: pick the last (most derived/specific) type
|
|
154
|
+
recurseInline(effectiveTypes->Array.getUnsafe(Array.length(effectiveTypes) - 1))
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
"JSON.t"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
hasNull ? `option<${result}>` : result
|
|
161
|
+
}
|
|
162
|
+
| Intersection(types) =>
|
|
163
|
+
// Support for intersections: merge object properties or pick last reference
|
|
164
|
+
if types->Array.every(t =>
|
|
165
|
+
switch t {
|
|
166
|
+
| Reference(_) => true
|
|
167
|
+
| _ => false
|
|
168
|
+
}
|
|
169
|
+
) && Array.length(types) > 0 {
|
|
170
|
+
generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline, types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
171
|
+
} else if inline {
|
|
172
|
+
// Extract complex intersection as a separate type
|
|
173
|
+
let baseName = ctx.path->String.split(".")->Array.get(ctx.path->String.split(".")->Array.length - 1)->Option.getOr("intersection")
|
|
174
|
+
let typeName = GenerationContext.extractType(ctx, ~baseName, irType)
|
|
175
|
+
typeName
|
|
176
|
+
} else {
|
|
177
|
+
// Try to merge all Object types in the intersection
|
|
178
|
+
let (objectProps, nonObjectTypes) = types->Array.reduce(
|
|
179
|
+
([], []),
|
|
180
|
+
((props, nonObj), t) =>
|
|
181
|
+
switch t {
|
|
182
|
+
| Object({properties}) => (Array.concat(props, properties), nonObj)
|
|
183
|
+
| _ => (props, Array.concat(nonObj, [t]))
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
if Array.length(objectProps) > 0 && Array.length(nonObjectTypes) == 0 {
|
|
187
|
+
// All objects: merge properties
|
|
188
|
+
let fields =
|
|
189
|
+
objectProps
|
|
190
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
191
|
+
let typeCode = recurseInline(fieldType)
|
|
192
|
+
let alreadyNullable = String.startsWith(typeCode, "option<") || switch fieldType {
|
|
193
|
+
| Option(_) => true
|
|
194
|
+
| Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
|
|
195
|
+
| _ => false
|
|
196
|
+
}
|
|
197
|
+
let finalType = isRequired || alreadyNullable ? typeCode : `option<${typeCode}>`
|
|
198
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
199
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
200
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
201
|
+
` ${aliasAnnotation}${escapedName}: ${finalType},`
|
|
202
|
+
})
|
|
203
|
+
->Array.join("\n")
|
|
204
|
+
`{\n${fields}\n}`
|
|
205
|
+
} else if Array.length(nonObjectTypes) > 0 && Array.length(objectProps) == 0 {
|
|
206
|
+
// No objects: pick last type as best effort
|
|
207
|
+
generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline, types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
208
|
+
} else {
|
|
209
|
+
addWarning(
|
|
210
|
+
ctx,
|
|
211
|
+
IntersectionNotFullySupported({location: ctx.path, note: "Mixed object/non-object intersection"}),
|
|
212
|
+
)
|
|
213
|
+
// Merge what we can, ignore non-object parts
|
|
214
|
+
let fields =
|
|
215
|
+
objectProps
|
|
216
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
217
|
+
let typeCode = recurseInline(fieldType)
|
|
218
|
+
let alreadyNullable = String.startsWith(typeCode, "option<") || switch fieldType {
|
|
219
|
+
| Option(_) => true
|
|
220
|
+
| Union(types) => types->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
|
|
221
|
+
| _ => false
|
|
222
|
+
}
|
|
223
|
+
let finalType = isRequired || alreadyNullable ? typeCode : `option<${typeCode}>`
|
|
224
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
225
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
226
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
227
|
+
` ${aliasAnnotation}${escapedName}: ${finalType},`
|
|
228
|
+
})
|
|
229
|
+
->Array.join("\n")
|
|
230
|
+
`{\n${fields}\n}`
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
| Option(inner) => `option<${recurseInline(inner)}>`
|
|
234
|
+
| Reference(ref) =>
|
|
235
|
+
// After IR normalization, ref may be just the schema name (no path prefix)
|
|
236
|
+
// Extract the name from the ref (handles both "Name" and "#/components/schemas/Name")
|
|
237
|
+
let refName = if ref->String.includes("/") {
|
|
238
|
+
ref->String.split("/")->Array.get(ref->String.split("/")->Array.length - 1)->Option.getOr("")
|
|
239
|
+
} else {
|
|
240
|
+
ref
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Detect self-reference using selfRefName from context
|
|
244
|
+
let isSelfRef = switch ctx.selfRefName {
|
|
245
|
+
| Some(selfName) => refName == selfName
|
|
246
|
+
| None => false
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if isSelfRef {
|
|
250
|
+
"t" // Use recursive self-reference
|
|
251
|
+
} else {
|
|
252
|
+
let typePath = switch ctx.availableSchemas {
|
|
253
|
+
| Some(available) =>
|
|
254
|
+
available->Array.includes(refName)
|
|
255
|
+
? `${CodegenUtils.toPascalCase(refName)}.t`
|
|
256
|
+
: `ComponentSchemas.${CodegenUtils.toPascalCase(refName)}.t`
|
|
257
|
+
| None =>
|
|
258
|
+
ReferenceResolver.refToTypePath(
|
|
259
|
+
~insideComponentSchemas=ctx.insideComponentSchemas,
|
|
260
|
+
~modulePrefix=ctx.modulePrefix,
|
|
261
|
+
ref,
|
|
262
|
+
)->Option.getOr("JSON.t")
|
|
263
|
+
}
|
|
264
|
+
if typePath == "JSON.t" {
|
|
265
|
+
addWarning(
|
|
266
|
+
ctx,
|
|
267
|
+
FallbackToJson({
|
|
268
|
+
reason: `Unresolved ref: ${ref}`,
|
|
269
|
+
context: {path: ctx.path, operation: "gen ref", schema: None},
|
|
270
|
+
}),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
typePath
|
|
274
|
+
}
|
|
275
|
+
| Unknown => "JSON.t"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Generate @unboxed variant body from a Union IR type.
|
|
281
|
+
// Each member must have a distinct runtime representation (validated by canUnbox check).
|
|
282
|
+
let generateUnboxedVariantBody = (~ctx: GenerationContext.t, types: array<SchemaIR.irType>): string => {
|
|
283
|
+
let rawNames = types->Array.map(CodegenUtils.variantConstructorName)
|
|
284
|
+
let names = CodegenUtils.deduplicateNames(rawNames)
|
|
285
|
+
|
|
286
|
+
types->Array.mapWithIndex((irType, i) => {
|
|
287
|
+
let constructorName = names->Array.getUnsafe(i)
|
|
288
|
+
let payloadType = switch irType {
|
|
289
|
+
| Object({properties, additionalProperties}) =>
|
|
290
|
+
if Array.length(properties) == 0 {
|
|
291
|
+
switch additionalProperties {
|
|
292
|
+
| Some(valueType) => {
|
|
293
|
+
let innerType = generateTypeWithContext(~ctx, ~depth=1, ~inline=true, valueType)
|
|
294
|
+
`(dict<${innerType}>)`
|
|
295
|
+
}
|
|
296
|
+
| None => `(dict<JSON.t>)`
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
let fields = properties->Array.map(((name, fieldType, isRequired)) => {
|
|
300
|
+
let typeCode = generateTypeWithContext(~ctx, ~depth=1, ~inline=true, fieldType)
|
|
301
|
+
let alreadyNullable = String.startsWith(typeCode, "option<") || switch fieldType {
|
|
302
|
+
| Option(_) => true
|
|
303
|
+
| Union(unionTypes) => unionTypes->Array.some(t => switch t { | Null | Literal(NullLiteral) => true | _ => false })
|
|
304
|
+
| _ => false
|
|
305
|
+
}
|
|
306
|
+
let finalType = isRequired || alreadyNullable ? typeCode : `option<${typeCode}>`
|
|
307
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
308
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
309
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
310
|
+
`${aliasAnnotation}${escapedName}: ${finalType}`
|
|
311
|
+
})->Array.join(", ")
|
|
312
|
+
`({${fields}})`
|
|
313
|
+
}
|
|
314
|
+
| _ =>
|
|
315
|
+
let innerType = generateTypeWithContext(~ctx, ~depth=1, ~inline=true, irType)
|
|
316
|
+
`(${innerType})`
|
|
317
|
+
}
|
|
318
|
+
`${constructorName}${payloadType}`
|
|
319
|
+
})->Array.join(" | ")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let generateType = (
|
|
323
|
+
~depth=0,
|
|
324
|
+
~path="",
|
|
325
|
+
~insideComponentSchemas=false,
|
|
326
|
+
~availableSchemas=?,
|
|
327
|
+
~modulePrefix="",
|
|
328
|
+
irType,
|
|
329
|
+
) => {
|
|
330
|
+
let ctx = GenerationContext.make(~path, ~insideComponentSchemas, ~availableSchemas?, ~modulePrefix, ())
|
|
331
|
+
(generateTypeWithContext(~ctx, ~depth, irType), ctx.warnings)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let generateNamedType = (
|
|
335
|
+
~namedSchema: SchemaIR.namedSchema,
|
|
336
|
+
~insideComponentSchemas=false,
|
|
337
|
+
~availableSchemas=?,
|
|
338
|
+
~modulePrefix="",
|
|
339
|
+
) => {
|
|
340
|
+
let ctx = GenerationContext.make(
|
|
341
|
+
~path=`type.${namedSchema.name}`,
|
|
342
|
+
~insideComponentSchemas,
|
|
343
|
+
~availableSchemas?,
|
|
344
|
+
~modulePrefix,
|
|
345
|
+
(),
|
|
346
|
+
)
|
|
347
|
+
let doc = switch namedSchema.description {
|
|
348
|
+
| Some(d) => CodegenUtils.generateDocString(~description=d, ())
|
|
349
|
+
| None => ""
|
|
350
|
+
}
|
|
351
|
+
let mainType = generateTypeWithContext(~ctx, ~depth=0, namedSchema.type_)
|
|
352
|
+
|
|
353
|
+
// Iteratively resolve extracted types (handles nested extraction).
|
|
354
|
+
// Use the same ctx so all nested extractions accumulate in ctx.extractedTypes
|
|
355
|
+
// and dedup works correctly.
|
|
356
|
+
let processed = ref(0)
|
|
357
|
+
while processed.contents < Array.length(ctx.extractedTypes) {
|
|
358
|
+
let idx = processed.contents
|
|
359
|
+
let {irType, isUnboxed, _}: GenerationContext.extractedType = ctx.extractedTypes->Array.getUnsafe(idx)
|
|
360
|
+
if !isUnboxed {
|
|
361
|
+
// Generate at top level to discover nested extractions
|
|
362
|
+
ignore(generateTypeWithContext(~ctx, ~depth=0, ~inline=false, irType))
|
|
363
|
+
} else {
|
|
364
|
+
// For unboxed variants, walk union members to discover nested extractions
|
|
365
|
+
switch irType {
|
|
366
|
+
| Union(types) =>
|
|
367
|
+
types->Array.forEach(memberType => {
|
|
368
|
+
ignore(generateTypeWithContext(~ctx, ~depth=0, ~inline=true, memberType))
|
|
369
|
+
})
|
|
370
|
+
| _ => ignore(generateTypeWithContext(~ctx, ~depth=0, ~inline=false, irType))
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
processed := idx + 1
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let allExtracted = Array.copy(ctx.extractedTypes)
|
|
377
|
+
|
|
378
|
+
// Generate final code for each extracted type.
|
|
379
|
+
let extractedDefs = allExtracted->Array.map(({typeName, irType, isUnboxed}: GenerationContext.extractedType) => {
|
|
380
|
+
if isUnboxed {
|
|
381
|
+
switch irType {
|
|
382
|
+
| Union(types) =>
|
|
383
|
+
let body = generateUnboxedVariantBody(~ctx, types)
|
|
384
|
+
`@unboxed type ${typeName} = ${body}`
|
|
385
|
+
| _ =>
|
|
386
|
+
let auxType = generateTypeWithContext(~ctx, ~depth=0, irType)
|
|
387
|
+
`type ${typeName} = ${auxType}`
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
let auxType = generateTypeWithContext(~ctx, ~depth=0, irType)
|
|
391
|
+
`type ${typeName} = ${auxType}`
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// Reverse so deeper-nested types are defined first (dependencies before dependents)
|
|
396
|
+
let reversedExtracted = allExtracted->Array.toReversed
|
|
397
|
+
|
|
398
|
+
let allDefs = Array.concat(extractedDefs->Array.toReversed, [`${doc}type ${namedSchema.name} = ${mainType}`])
|
|
399
|
+
(
|
|
400
|
+
allDefs->Array.join("\n\n"),
|
|
401
|
+
ctx.warnings,
|
|
402
|
+
reversedExtracted,
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let generateAllTypes = (~context: SchemaIR.schemaContext) => {
|
|
407
|
+
let warnings = []
|
|
408
|
+
let types =
|
|
409
|
+
Dict.valuesToArray(context.schemas)
|
|
410
|
+
->Array.toSorted((a, b) => String.compare(a.name, b.name))
|
|
411
|
+
->Array.map(s => {
|
|
412
|
+
let (code, w, _) = generateNamedType(~namedSchema=s)
|
|
413
|
+
warnings->Array.pushMany(w)
|
|
414
|
+
code
|
|
415
|
+
})
|
|
416
|
+
(types, warnings)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let generateTypeAndSchema = (~namedSchema) => {
|
|
420
|
+
let (tCode, tW, extractedTypes) = generateNamedType(~namedSchema)
|
|
421
|
+
let (sCode, sW) = IRToSuryGenerator.generateNamedSchema(~namedSchema, ~extractedTypes)
|
|
422
|
+
((tCode, sCode), Array.concat(tW, sW))
|
|
423
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// IRToTypeScriptGenerator.res - Convert SchemaIR to TypeScript types
|
|
4
|
+
|
|
5
|
+
let rec generateType = (~irType: SchemaIR.irType, ~isOptional=false) =>
|
|
6
|
+
switch irType {
|
|
7
|
+
| String(_) => "string"
|
|
8
|
+
| Number(_)
|
|
9
|
+
| Integer(_) => "number"
|
|
10
|
+
| Boolean => "boolean"
|
|
11
|
+
| Null => "null"
|
|
12
|
+
| Unknown => "unknown"
|
|
13
|
+
| Array({items}) => `${generateType(~irType=items)}[]`
|
|
14
|
+
| Object({properties, additionalProperties}) =>
|
|
15
|
+
generateObjectType(~properties, ~additionalProperties)
|
|
16
|
+
| Literal(literal) =>
|
|
17
|
+
switch literal {
|
|
18
|
+
| StringLiteral(s) => `"${s}"`
|
|
19
|
+
| NumberLiteral(n) => Float.toString(n)
|
|
20
|
+
| BooleanLiteral(b) => b ? "true" : "false"
|
|
21
|
+
| NullLiteral => "null"
|
|
22
|
+
}
|
|
23
|
+
| Union(types) => types->Array.map(t => generateType(~irType=t))->Array.join(" | ")
|
|
24
|
+
| Intersection(types) => types->Array.map(t => generateType(~irType=t))->Array.join(" & ")
|
|
25
|
+
| Reference(ref) =>
|
|
26
|
+
switch String.split(ref, "/") {
|
|
27
|
+
| [_, "components", "schemas", name] => `ComponentSchemas.${name}`
|
|
28
|
+
| _ => ref
|
|
29
|
+
}
|
|
30
|
+
| Option(inner) =>
|
|
31
|
+
isOptional ? generateType(~irType=inner, ~isOptional=true) : `${generateType(~irType=inner)} | undefined`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
and generateObjectType = (~properties, ~additionalProperties) => {
|
|
35
|
+
let propertyLines = properties->Array.map(((name, fieldType, isRequired)) => {
|
|
36
|
+
let (actualType, isFieldOptional) = switch fieldType {
|
|
37
|
+
| SchemaIR.Option(inner) => (inner, true)
|
|
38
|
+
| _ => (fieldType, !isRequired)
|
|
39
|
+
}
|
|
40
|
+
` ${name}${isFieldOptional ? "?" : ""}: ${generateType(~irType=actualType, ~isOptional=true)};`
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
let additionalPropertiesLines =
|
|
44
|
+
additionalProperties->Option.mapOr([], valueType => [
|
|
45
|
+
` [key: string]: ${generateType(~irType=valueType)};`,
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
let allLines = Array.concat(propertyLines, additionalPropertiesLines)
|
|
49
|
+
|
|
50
|
+
if allLines->Array.length == 0 {
|
|
51
|
+
"Record<string, never>"
|
|
52
|
+
} else {
|
|
53
|
+
`{\n${allLines->Array.join("\n")}\n}`
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let generateNamedType = (~namedSchema: SchemaIR.namedSchema) => {
|
|
58
|
+
let docComment = namedSchema.description->Option.mapOr("", description => `/** ${description} */\n`)
|
|
59
|
+
let typeCode = generateType(~irType=namedSchema.type_)
|
|
60
|
+
|
|
61
|
+
let declaration = switch namedSchema.type_ {
|
|
62
|
+
| Object(_) =>
|
|
63
|
+
if typeCode == "Record<string, never>" {
|
|
64
|
+
`export type ${namedSchema.name} = ${typeCode};`
|
|
65
|
+
} else {
|
|
66
|
+
`export interface ${namedSchema.name} ${typeCode}`
|
|
67
|
+
}
|
|
68
|
+
| _ => `export type ${namedSchema.name} = ${typeCode};`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
docComment ++ declaration
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let generateParameterType = (~name, ~schema: Types.jsonSchema) => {
|
|
75
|
+
let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
|
|
76
|
+
(CodegenUtils.toCamelCase(name), generateType(~irType=ir))
|
|
77
|
+
}
|