@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
|
@@ -5,13 +5,17 @@ open Types
|
|
|
5
5
|
|
|
6
6
|
let addWarning = GenerationContext.addWarning
|
|
7
7
|
|
|
8
|
-
|
|
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 => {
|
|
9
12
|
// We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
|
|
10
13
|
if depth > 100 {
|
|
11
14
|
addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
|
|
12
15
|
"JSON.t"
|
|
13
16
|
} else {
|
|
14
|
-
|
|
17
|
+
// Inside type constructors, records/variants can't appear; recurse as inline
|
|
18
|
+
let recurseInline = nextIrType => generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline=true, nextIrType)
|
|
15
19
|
|
|
16
20
|
switch irType {
|
|
17
21
|
| String(_) => "string"
|
|
@@ -19,19 +23,30 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
19
23
|
| Integer(_) => "int"
|
|
20
24
|
| Boolean => "bool"
|
|
21
25
|
| Null => "unit"
|
|
22
|
-
| Array({items}) => `array<${
|
|
26
|
+
| Array({items}) => `array<${recurseInline(items)}>`
|
|
23
27
|
| Object({properties, additionalProperties}) =>
|
|
24
28
|
if Array.length(properties) == 0 {
|
|
25
29
|
switch additionalProperties {
|
|
26
|
-
| Some(valueType) => `dict<${
|
|
27
|
-
| None => "JSON.t"
|
|
30
|
+
| Some(valueType) => `dict<${recurseInline(valueType)}>`
|
|
31
|
+
| None => "dict<JSON.t>"
|
|
28
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
|
|
29
38
|
} else {
|
|
30
39
|
let fields =
|
|
31
40
|
properties
|
|
32
41
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
33
|
-
let typeCode =
|
|
34
|
-
|
|
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}>`
|
|
35
50
|
let camelName = name->CodegenUtils.toCamelCase
|
|
36
51
|
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
37
52
|
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
@@ -59,7 +74,7 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
59
74
|
|
|
60
75
|
// If the union is just [T, null], treat as option<T>
|
|
61
76
|
if hasNull && Array.length(nonNullTypes) == 1 {
|
|
62
|
-
let inner =
|
|
77
|
+
let inner = generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline=true, nonNullTypes->Array.getUnsafe(0))
|
|
63
78
|
`option<${inner}>`
|
|
64
79
|
} else {
|
|
65
80
|
// Work with the non-null types (re-wrap in option at the end if hasNull)
|
|
@@ -81,7 +96,7 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
81
96
|
Array.length(effectiveTypes) == 2 &&
|
|
82
97
|
SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
|
|
83
98
|
) {
|
|
84
|
-
`array<${
|
|
99
|
+
`array<${recurseInline(Option.getOr(arrayItemType, Unknown))}>`
|
|
85
100
|
} else if (
|
|
86
101
|
effectiveTypes->Array.every(t =>
|
|
87
102
|
switch t {
|
|
@@ -92,6 +107,7 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
92
107
|
Array.length(effectiveTypes) > 0 &&
|
|
93
108
|
Array.length(effectiveTypes) <= 50
|
|
94
109
|
) {
|
|
110
|
+
// Polymorphic variants: valid inline
|
|
95
111
|
let variants =
|
|
96
112
|
effectiveTypes
|
|
97
113
|
->Array.map(t =>
|
|
@@ -103,26 +119,40 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
103
119
|
->Array.join(" | ")
|
|
104
120
|
`[${variants}]`
|
|
105
121
|
} else if Array.length(effectiveTypes) > 0 {
|
|
106
|
-
//
|
|
107
|
-
let
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
(CodegenUtils.toPascalCase(name), recurse(t))
|
|
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"
|
|
119
134
|
}
|
|
120
|
-
|
|
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
|
|
121
148
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
156
|
} else {
|
|
127
157
|
"JSON.t"
|
|
128
158
|
}
|
|
@@ -137,7 +167,12 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
137
167
|
| _ => false
|
|
138
168
|
}
|
|
139
169
|
) && Array.length(types) > 0 {
|
|
140
|
-
|
|
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
|
|
141
176
|
} else {
|
|
142
177
|
// Try to merge all Object types in the intersection
|
|
143
178
|
let (objectProps, nonObjectTypes) = types->Array.reduce(
|
|
@@ -153,8 +188,13 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
153
188
|
let fields =
|
|
154
189
|
objectProps
|
|
155
190
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
156
|
-
let typeCode =
|
|
157
|
-
let
|
|
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}>`
|
|
158
198
|
let camelName = name->CodegenUtils.toCamelCase
|
|
159
199
|
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
160
200
|
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
@@ -164,7 +204,7 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
164
204
|
`{\n${fields}\n}`
|
|
165
205
|
} else if Array.length(nonObjectTypes) > 0 && Array.length(objectProps) == 0 {
|
|
166
206
|
// No objects: pick last type as best effort
|
|
167
|
-
|
|
207
|
+
generateTypeWithContext(~ctx, ~depth=depth + 1, ~inline, types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
168
208
|
} else {
|
|
169
209
|
addWarning(
|
|
170
210
|
ctx,
|
|
@@ -174,8 +214,13 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
174
214
|
let fields =
|
|
175
215
|
objectProps
|
|
176
216
|
->Array.map(((name, fieldType, isRequired)) => {
|
|
177
|
-
let typeCode =
|
|
178
|
-
let
|
|
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}>`
|
|
179
224
|
let camelName = name->CodegenUtils.toCamelCase
|
|
180
225
|
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
181
226
|
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
@@ -185,40 +230,95 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
185
230
|
`{\n${fields}\n}`
|
|
186
231
|
}
|
|
187
232
|
}
|
|
188
|
-
| Option(inner) => `option<${
|
|
233
|
+
| Option(inner) => `option<${recurseInline(inner)}>`
|
|
189
234
|
| Reference(ref) =>
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
->Option.getOr("")
|
|
197
|
-
available->Array.includes(name)
|
|
198
|
-
? `${CodegenUtils.toPascalCase(name)}.t`
|
|
199
|
-
: `ComponentSchemas.${CodegenUtils.toPascalCase(name)}.t`
|
|
200
|
-
| None =>
|
|
201
|
-
ReferenceResolver.refToTypePath(
|
|
202
|
-
~insideComponentSchemas=ctx.insideComponentSchemas,
|
|
203
|
-
~modulePrefix=ctx.modulePrefix,
|
|
204
|
-
ref,
|
|
205
|
-
)->Option.getOr("JSON.t")
|
|
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
|
|
206
241
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
215
274
|
}
|
|
216
|
-
typePath
|
|
217
275
|
| Unknown => "JSON.t"
|
|
218
276
|
}
|
|
219
277
|
}
|
|
220
278
|
}
|
|
221
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
|
+
|
|
222
322
|
let generateType = (
|
|
223
323
|
~depth=0,
|
|
224
324
|
~path="",
|
|
@@ -248,9 +348,58 @@ let generateNamedType = (
|
|
|
248
348
|
| Some(d) => CodegenUtils.generateDocString(~description=d, ())
|
|
249
349
|
| None => ""
|
|
250
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}`])
|
|
251
399
|
(
|
|
252
|
-
|
|
400
|
+
allDefs->Array.join("\n\n"),
|
|
253
401
|
ctx.warnings,
|
|
402
|
+
reversedExtracted,
|
|
254
403
|
)
|
|
255
404
|
}
|
|
256
405
|
|
|
@@ -260,7 +409,7 @@ let generateAllTypes = (~context: SchemaIR.schemaContext) => {
|
|
|
260
409
|
Dict.valuesToArray(context.schemas)
|
|
261
410
|
->Array.toSorted((a, b) => String.compare(a.name, b.name))
|
|
262
411
|
->Array.map(s => {
|
|
263
|
-
let (code, w) = generateNamedType(~namedSchema=s)
|
|
412
|
+
let (code, w, _) = generateNamedType(~namedSchema=s)
|
|
264
413
|
warnings->Array.pushMany(w)
|
|
265
414
|
code
|
|
266
415
|
})
|
|
@@ -268,7 +417,7 @@ let generateAllTypes = (~context: SchemaIR.schemaContext) => {
|
|
|
268
417
|
}
|
|
269
418
|
|
|
270
419
|
let generateTypeAndSchema = (~namedSchema) => {
|
|
271
|
-
let (tCode, tW) = generateNamedType(~namedSchema)
|
|
272
|
-
let (sCode, sW) = IRToSuryGenerator.generateNamedSchema(~namedSchema)
|
|
420
|
+
let (tCode, tW, extractedTypes) = generateNamedType(~namedSchema)
|
|
421
|
+
let (sCode, sW) = IRToSuryGenerator.generateNamedSchema(~namedSchema, ~extractedTypes)
|
|
273
422
|
((tCode, sCode), Array.concat(tW, sW))
|
|
274
423
|
}
|
|
@@ -59,7 +59,12 @@ let generateNamedType = (~namedSchema: SchemaIR.namedSchema) => {
|
|
|
59
59
|
let typeCode = generateType(~irType=namedSchema.type_)
|
|
60
60
|
|
|
61
61
|
let declaration = switch namedSchema.type_ {
|
|
62
|
-
| Object(_) =>
|
|
62
|
+
| Object(_) =>
|
|
63
|
+
if typeCode == "Record<string, never>" {
|
|
64
|
+
`export type ${namedSchema.name} = ${typeCode};`
|
|
65
|
+
} else {
|
|
66
|
+
`export interface ${namedSchema.name} ${typeCode}`
|
|
67
|
+
}
|
|
63
68
|
| _ => `export type ${namedSchema.name} = ${typeCode};`
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -8,11 +8,12 @@ let generateSchemaCodeForDict = (schemaDict: dict<jsonSchema>) =>
|
|
|
8
8
|
->Array.toSorted(((nameA, _), (nameB, _)) => String.compare(nameA, nameB))
|
|
9
9
|
->Array.flatMap(((name, schema)) => {
|
|
10
10
|
let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
|
|
11
|
-
let (typeCode, _) = IRToTypeGenerator.generateNamedType(
|
|
11
|
+
let (typeCode, _, extractedTypes) = IRToTypeGenerator.generateNamedType(
|
|
12
12
|
~namedSchema={name: name, description: schema.description, type_: ir},
|
|
13
13
|
)
|
|
14
14
|
let (schemaCode, _) = IRToSuryGenerator.generateNamedSchema(
|
|
15
15
|
~namedSchema={name: `${name}Schema`, description: schema.description, type_: ir},
|
|
16
|
+
~extractedTypes,
|
|
16
17
|
)
|
|
17
18
|
[typeCode->CodegenUtils.indent(2), schemaCode->CodegenUtils.indent(2), ""]
|
|
18
19
|
})
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
let generateTypeCodeAndSchemaCode = (name, schema: Types.jsonSchema) => {
|
|
6
6
|
let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
|
|
7
|
-
let (typeCode, _) = IRToTypeGenerator.generateNamedType(
|
|
7
|
+
let (typeCode, _, extractedTypes) = IRToTypeGenerator.generateNamedType(
|
|
8
8
|
~namedSchema={name: name, description: schema.description, type_: ir},
|
|
9
9
|
)
|
|
10
10
|
let (schemaCode, _) = IRToSuryGenerator.generateNamedSchema(
|
|
11
11
|
~namedSchema={name: `${name}Schema`, description: schema.description, type_: ir},
|
|
12
|
+
~extractedTypes,
|
|
12
13
|
)
|
|
13
14
|
`${typeCode}\n\n${schemaCode}`
|
|
14
15
|
}
|
|
@@ -2,22 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
// GenerationContext.res - Shared context type for IR code generators
|
|
4
4
|
|
|
5
|
+
// Extracted auxiliary type that was too complex to inline
|
|
6
|
+
type extractedType = {
|
|
7
|
+
typeName: string,
|
|
8
|
+
irType: SchemaIR.irType,
|
|
9
|
+
isUnboxed: bool, // Needs @unboxed annotation (for variant types from mixed unions)
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
type t = {
|
|
6
13
|
mutable warnings: array<CodegenError.Warning.t>,
|
|
14
|
+
mutable extractedTypes: array<extractedType>,
|
|
15
|
+
mutable extractCounter: int,
|
|
7
16
|
path: string,
|
|
8
17
|
insideComponentSchemas: bool, // Whether we're generating inside ComponentSchemas module
|
|
9
18
|
availableSchemas: option<array<string>>, // Schemas available in current module (for fork schemas)
|
|
10
19
|
modulePrefix: string, // Module prefix for qualified references (e.g., "MisskeyIo")
|
|
20
|
+
selfRefName: option<string>, // Schema name for self-referential type detection (e.g., "DriveFolder")
|
|
11
21
|
}
|
|
12
22
|
|
|
13
|
-
let make = (~path, ~insideComponentSchemas=false, ~availableSchemas=?, ~modulePrefix="", ()): t => {
|
|
23
|
+
let make = (~path, ~insideComponentSchemas=false, ~availableSchemas=?, ~modulePrefix="", ~selfRefName=?, ()): t => {
|
|
14
24
|
warnings: [],
|
|
25
|
+
extractedTypes: [],
|
|
26
|
+
extractCounter: 0,
|
|
15
27
|
path,
|
|
16
28
|
insideComponentSchemas,
|
|
17
29
|
availableSchemas,
|
|
18
30
|
modulePrefix,
|
|
31
|
+
selfRefName,
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
let addWarning = (ctx: t, warning: CodegenError.Warning.t): unit => {
|
|
22
35
|
ctx.warnings->Array.push(warning)
|
|
23
36
|
}
|
|
37
|
+
|
|
38
|
+
let extractType = (ctx: t, ~baseName: string, ~isUnboxed=false, irType: SchemaIR.irType): string => {
|
|
39
|
+
// Check if this irType was already extracted (avoid duplicates)
|
|
40
|
+
let existing = ctx.extractedTypes->Array.find(({irType: existingIr}: extractedType) =>
|
|
41
|
+
SchemaIR.equals(existingIr, irType)
|
|
42
|
+
)
|
|
43
|
+
switch existing {
|
|
44
|
+
| Some({typeName}) => typeName
|
|
45
|
+
| None =>
|
|
46
|
+
ctx.extractCounter = ctx.extractCounter + 1
|
|
47
|
+
// ReScript type names must start with lowercase
|
|
48
|
+
let lowerBaseName = switch baseName->String.charAt(0) {
|
|
49
|
+
| "" => "extracted"
|
|
50
|
+
| first => first->String.toLowerCase ++ baseName->String.sliceToEnd(~start=1)
|
|
51
|
+
}
|
|
52
|
+
let typeName = `${lowerBaseName}_${Int.toString(ctx.extractCounter)}`
|
|
53
|
+
ctx.extractedTypes->Array.push({typeName, irType, isUnboxed})
|
|
54
|
+
typeName
|
|
55
|
+
}
|
|
56
|
+
}
|