@f3liz/rescript-autogen-openapi 0.1.7 → 0.2.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.
|
@@ -97,7 +97,22 @@ function generateSchemaWithContext(ctx, depthOpt, irType) {
|
|
|
97
97
|
}
|
|
98
98
|
case "Union" :
|
|
99
99
|
let types = irType._0;
|
|
100
|
-
let
|
|
100
|
+
let nonNullTypes = types.filter(t => {
|
|
101
|
+
if (typeof t !== "object") {
|
|
102
|
+
return t !== "Null";
|
|
103
|
+
}
|
|
104
|
+
if (t.TAG !== "Literal") {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
let tmp = t._0;
|
|
108
|
+
return typeof tmp === "object";
|
|
109
|
+
});
|
|
110
|
+
let hasNull = nonNullTypes.length < types.length;
|
|
111
|
+
if (hasNull && nonNullTypes.length === 1) {
|
|
112
|
+
return `S.nullableAsOption(` + recurse(nonNullTypes[0]) + `)`;
|
|
113
|
+
}
|
|
114
|
+
let effectiveTypes = hasNull ? nonNullTypes : types;
|
|
115
|
+
let match = Stdlib_Array.reduce(effectiveTypes, [
|
|
101
116
|
false,
|
|
102
117
|
false,
|
|
103
118
|
undefined,
|
|
@@ -122,39 +137,97 @@ function generateSchemaWithContext(ctx, depthOpt, irType) {
|
|
|
122
137
|
}
|
|
123
138
|
});
|
|
124
139
|
let arrayItemType = match[2];
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
let result = match[0] && match[1] && effectiveTypes.length === 2 && SchemaIR.equals(Stdlib_Option.getOr(arrayItemType, "Unknown"), Stdlib_Option.getOr(match[3], "Unknown")) ? `S.array(` + recurse(Stdlib_Option.getOr(arrayItemType, "Unknown")) + `)` : (
|
|
141
|
+
effectiveTypes.every(t => {
|
|
142
|
+
if (typeof t !== "object") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (t.TAG !== "Literal") {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
let tmp = t._0;
|
|
149
|
+
if (typeof tmp !== "object") {
|
|
150
|
+
return false;
|
|
151
|
+
} else {
|
|
152
|
+
return tmp.TAG === "StringLiteral";
|
|
153
|
+
}
|
|
154
|
+
}) && effectiveTypes.length !== 0 && effectiveTypes.length <= 50 ? `S.union([` + effectiveTypes.map(recurse).join(", ") + `])` : (
|
|
155
|
+
effectiveTypes.length !== 0 ? `S.union([` + effectiveTypes.map(recurse).join(", ") + `])` : "S.json"
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
if (hasNull) {
|
|
159
|
+
return `S.nullableAsOption(` + result + `)`;
|
|
160
|
+
} else {
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
case "Intersection" :
|
|
164
|
+
let types$1 = irType._0;
|
|
165
|
+
if (types$1.every(t => {
|
|
128
166
|
if (typeof t !== "object") {
|
|
129
167
|
return false;
|
|
168
|
+
} else {
|
|
169
|
+
return t.TAG === "Reference";
|
|
130
170
|
}
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
}) && types$1.length !== 0) {
|
|
172
|
+
return recurse(Stdlib_Option.getOr(types$1[types$1.length - 1 | 0], "Unknown"));
|
|
173
|
+
}
|
|
174
|
+
let match$1 = Stdlib_Array.reduce(types$1, [
|
|
175
|
+
[],
|
|
176
|
+
[]
|
|
177
|
+
], (param, t) => {
|
|
178
|
+
let nonObj = param[1];
|
|
179
|
+
let props = param[0];
|
|
180
|
+
if (typeof t !== "object") {
|
|
181
|
+
return [
|
|
182
|
+
props,
|
|
183
|
+
nonObj.concat([t])
|
|
184
|
+
];
|
|
185
|
+
} else if (t.TAG === "Object") {
|
|
186
|
+
return [
|
|
187
|
+
props.concat(t.properties),
|
|
188
|
+
nonObj
|
|
189
|
+
];
|
|
190
|
+
} else {
|
|
191
|
+
return [
|
|
192
|
+
props,
|
|
193
|
+
nonObj.concat([t])
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
let nonObjectTypes = match$1[1];
|
|
198
|
+
let objectProps = match$1[0];
|
|
199
|
+
if (objectProps.length !== 0 && nonObjectTypes.length === 0) {
|
|
200
|
+
let fields$1 = objectProps.map(param => {
|
|
201
|
+
let name = param[0];
|
|
202
|
+
let schemaCode = recurse(param[1]);
|
|
203
|
+
let camelName = CodegenUtils.escapeKeyword(JsConvertCase.toCamelCase(name));
|
|
204
|
+
if (param[2]) {
|
|
205
|
+
return ` ` + camelName + `: s.field("` + name + `", ` + schemaCode + `),`;
|
|
206
|
+
} else {
|
|
207
|
+
return ` ` + camelName + `: s.fieldOr("` + name + `", S.nullableAsOption(` + schemaCode + `), None),`;
|
|
133
208
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}) && types.length !== 0 && types.length <= 50) {
|
|
137
|
-
return `S.union([` + types.map(recurse).join(", ") + `])`;
|
|
138
|
-
} else {
|
|
139
|
-
GenerationContext.addWarning(ctx, {
|
|
140
|
-
TAG: "ComplexUnionSimplified",
|
|
141
|
-
location: ctx.path,
|
|
142
|
-
types: types.map(SchemaIR.toString).join(" | ")
|
|
143
|
-
});
|
|
144
|
-
return "S.json";
|
|
209
|
+
}).join("\n");
|
|
210
|
+
return `S.object(s => {\n` + fields$1 + `\n })`;
|
|
145
211
|
}
|
|
146
|
-
|
|
147
|
-
let types$1 = irType._0;
|
|
148
|
-
if (types$1.every(t => typeof t !== "object" ? false : t.TAG === "Reference") && types$1.length !== 0) {
|
|
212
|
+
if (nonObjectTypes.length !== 0 && objectProps.length === 0) {
|
|
149
213
|
return recurse(Stdlib_Option.getOr(types$1[types$1.length - 1 | 0], "Unknown"));
|
|
150
|
-
} else {
|
|
151
|
-
GenerationContext.addWarning(ctx, {
|
|
152
|
-
TAG: "IntersectionNotFullySupported",
|
|
153
|
-
location: ctx.path,
|
|
154
|
-
note: "Complex intersection"
|
|
155
|
-
});
|
|
156
|
-
return "S.json";
|
|
157
214
|
}
|
|
215
|
+
GenerationContext.addWarning(ctx, {
|
|
216
|
+
TAG: "IntersectionNotFullySupported",
|
|
217
|
+
location: ctx.path,
|
|
218
|
+
note: "Mixed object/non-object intersection"
|
|
219
|
+
});
|
|
220
|
+
let fields$2 = objectProps.map(param => {
|
|
221
|
+
let name = param[0];
|
|
222
|
+
let schemaCode = recurse(param[1]);
|
|
223
|
+
let camelName = CodegenUtils.escapeKeyword(JsConvertCase.toCamelCase(name));
|
|
224
|
+
if (param[2]) {
|
|
225
|
+
return ` ` + camelName + `: s.field("` + name + `", ` + schemaCode + `),`;
|
|
226
|
+
} else {
|
|
227
|
+
return ` ` + camelName + `: s.fieldOr("` + name + `", S.nullableAsOption(` + schemaCode + `), None),`;
|
|
228
|
+
}
|
|
229
|
+
}).join("\n");
|
|
230
|
+
return `S.object(s => {\n` + fields$2 + `\n })`;
|
|
158
231
|
case "Reference" :
|
|
159
232
|
let ref = irType._0;
|
|
160
233
|
let available = ctx.availableSchemas;
|
|
@@ -75,7 +75,23 @@ function generateTypeWithContext(ctx, depthOpt, irType) {
|
|
|
75
75
|
}
|
|
76
76
|
case "Union" :
|
|
77
77
|
let types = irType._0;
|
|
78
|
-
let
|
|
78
|
+
let nonNullTypes = types.filter(t => {
|
|
79
|
+
if (typeof t !== "object") {
|
|
80
|
+
return t !== "Null";
|
|
81
|
+
}
|
|
82
|
+
if (t.TAG !== "Literal") {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
let tmp = t._0;
|
|
86
|
+
return typeof tmp === "object";
|
|
87
|
+
});
|
|
88
|
+
let hasNull = nonNullTypes.length < types.length;
|
|
89
|
+
if (hasNull && nonNullTypes.length === 1) {
|
|
90
|
+
let inner = recurse(nonNullTypes[0]);
|
|
91
|
+
return `option<` + inner + `>`;
|
|
92
|
+
}
|
|
93
|
+
let effectiveTypes = hasNull ? nonNullTypes : types;
|
|
94
|
+
let match = Stdlib_Array.reduce(effectiveTypes, [
|
|
79
95
|
false,
|
|
80
96
|
false,
|
|
81
97
|
undefined,
|
|
@@ -100,10 +116,10 @@ function generateTypeWithContext(ctx, depthOpt, irType) {
|
|
|
100
116
|
}
|
|
101
117
|
});
|
|
102
118
|
let arrayItemType = match[2];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
119
|
+
let result;
|
|
120
|
+
if (match[0] && match[1] && effectiveTypes.length === 2 && SchemaIR.equals(Stdlib_Option.getOr(arrayItemType, "Unknown"), Stdlib_Option.getOr(match[3], "Unknown"))) {
|
|
121
|
+
result = `array<` + recurse(Stdlib_Option.getOr(arrayItemType, "Unknown")) + `>`;
|
|
122
|
+
} else if (effectiveTypes.every(t => {
|
|
107
123
|
if (typeof t !== "object") {
|
|
108
124
|
return false;
|
|
109
125
|
}
|
|
@@ -116,8 +132,8 @@ function generateTypeWithContext(ctx, depthOpt, irType) {
|
|
|
116
132
|
} else {
|
|
117
133
|
return tmp.TAG === "StringLiteral";
|
|
118
134
|
}
|
|
119
|
-
}) &&
|
|
120
|
-
let variants =
|
|
135
|
+
}) && effectiveTypes.length !== 0 && effectiveTypes.length <= 50) {
|
|
136
|
+
let variants = effectiveTypes.map(t => {
|
|
121
137
|
if (typeof t !== "object") {
|
|
122
138
|
return "#Unknown";
|
|
123
139
|
}
|
|
@@ -131,26 +147,155 @@ function generateTypeWithContext(ctx, depthOpt, irType) {
|
|
|
131
147
|
return `#` + JsConvertCase.toPascalCase(s._0);
|
|
132
148
|
}
|
|
133
149
|
}).join(" | ");
|
|
134
|
-
|
|
150
|
+
result = `[` + variants + `]`;
|
|
151
|
+
} else if (effectiveTypes.length !== 0) {
|
|
152
|
+
let hasPrimitives = {
|
|
153
|
+
contents: false
|
|
154
|
+
};
|
|
155
|
+
let variantCases = effectiveTypes.map((t, i) => {
|
|
156
|
+
let match;
|
|
157
|
+
let exit = 0;
|
|
158
|
+
if (typeof t !== "object") {
|
|
159
|
+
if (t === "Boolean") {
|
|
160
|
+
hasPrimitives.contents = true;
|
|
161
|
+
match = [
|
|
162
|
+
"Bool",
|
|
163
|
+
"bool"
|
|
164
|
+
];
|
|
165
|
+
} else {
|
|
166
|
+
exit = 1;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
switch (t.TAG) {
|
|
170
|
+
case "String" :
|
|
171
|
+
hasPrimitives.contents = true;
|
|
172
|
+
match = [
|
|
173
|
+
"String",
|
|
174
|
+
"string"
|
|
175
|
+
];
|
|
176
|
+
break;
|
|
177
|
+
case "Number" :
|
|
178
|
+
hasPrimitives.contents = true;
|
|
179
|
+
match = [
|
|
180
|
+
"Float",
|
|
181
|
+
"float"
|
|
182
|
+
];
|
|
183
|
+
break;
|
|
184
|
+
case "Integer" :
|
|
185
|
+
hasPrimitives.contents = true;
|
|
186
|
+
match = [
|
|
187
|
+
"Int",
|
|
188
|
+
"int"
|
|
189
|
+
];
|
|
190
|
+
break;
|
|
191
|
+
case "Array" :
|
|
192
|
+
match = [
|
|
193
|
+
"Array",
|
|
194
|
+
`array<` + recurse(t.items) + `>`
|
|
195
|
+
];
|
|
196
|
+
break;
|
|
197
|
+
case "Object" :
|
|
198
|
+
match = [
|
|
199
|
+
"Object",
|
|
200
|
+
recurse(t)
|
|
201
|
+
];
|
|
202
|
+
break;
|
|
203
|
+
case "Reference" :
|
|
204
|
+
let ref = t._0;
|
|
205
|
+
let name = Stdlib_Option.getOr(ref.split("/")[ref.split("/").length - 1 | 0], "");
|
|
206
|
+
match = [
|
|
207
|
+
JsConvertCase.toPascalCase(name),
|
|
208
|
+
recurse(t)
|
|
209
|
+
];
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
exit = 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (exit === 1) {
|
|
216
|
+
match = [
|
|
217
|
+
`V` + i.toString(),
|
|
218
|
+
recurse(t)
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
return ` | ` + match[0] + `(` + match[1] + `)`;
|
|
222
|
+
});
|
|
223
|
+
let unboxedAttr = hasPrimitives.contents ? "@unboxed " : "";
|
|
224
|
+
result = unboxedAttr + `[\n` + variantCases.join("\n") + `\n]`;
|
|
225
|
+
} else {
|
|
226
|
+
result = "JSON.t";
|
|
227
|
+
}
|
|
228
|
+
if (hasNull) {
|
|
229
|
+
return `option<` + result + `>`;
|
|
230
|
+
} else {
|
|
231
|
+
return result;
|
|
135
232
|
}
|
|
136
|
-
GenerationContext.addWarning(ctx, {
|
|
137
|
-
TAG: "ComplexUnionSimplified",
|
|
138
|
-
location: ctx.path,
|
|
139
|
-
types: types.map(SchemaIR.toString).join(" | ")
|
|
140
|
-
});
|
|
141
|
-
return "JSON.t";
|
|
142
233
|
case "Intersection" :
|
|
143
234
|
let types$1 = irType._0;
|
|
144
|
-
if (types$1.every(t =>
|
|
235
|
+
if (types$1.every(t => {
|
|
236
|
+
if (typeof t !== "object") {
|
|
237
|
+
return false;
|
|
238
|
+
} else {
|
|
239
|
+
return t.TAG === "Reference";
|
|
240
|
+
}
|
|
241
|
+
}) && types$1.length !== 0) {
|
|
145
242
|
return recurse(Stdlib_Option.getOr(types$1[types$1.length - 1 | 0], "Unknown"));
|
|
146
|
-
} else {
|
|
147
|
-
GenerationContext.addWarning(ctx, {
|
|
148
|
-
TAG: "IntersectionNotFullySupported",
|
|
149
|
-
location: ctx.path,
|
|
150
|
-
note: "Complex intersection"
|
|
151
|
-
});
|
|
152
|
-
return "JSON.t";
|
|
153
243
|
}
|
|
244
|
+
let match$1 = Stdlib_Array.reduce(types$1, [
|
|
245
|
+
[],
|
|
246
|
+
[]
|
|
247
|
+
], (param, t) => {
|
|
248
|
+
let nonObj = param[1];
|
|
249
|
+
let props = param[0];
|
|
250
|
+
if (typeof t !== "object") {
|
|
251
|
+
return [
|
|
252
|
+
props,
|
|
253
|
+
nonObj.concat([t])
|
|
254
|
+
];
|
|
255
|
+
} else if (t.TAG === "Object") {
|
|
256
|
+
return [
|
|
257
|
+
props.concat(t.properties),
|
|
258
|
+
nonObj
|
|
259
|
+
];
|
|
260
|
+
} else {
|
|
261
|
+
return [
|
|
262
|
+
props,
|
|
263
|
+
nonObj.concat([t])
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
let nonObjectTypes = match$1[1];
|
|
268
|
+
let objectProps = match$1[0];
|
|
269
|
+
if (objectProps.length !== 0 && nonObjectTypes.length === 0) {
|
|
270
|
+
let fields$1 = objectProps.map(param => {
|
|
271
|
+
let name = param[0];
|
|
272
|
+
let typeCode = recurse(param[1]);
|
|
273
|
+
let finalType = param[2] ? typeCode : `option<` + typeCode + `>`;
|
|
274
|
+
let camelName = JsConvertCase.toCamelCase(name);
|
|
275
|
+
let escapedName = CodegenUtils.escapeKeyword(camelName);
|
|
276
|
+
let aliasAnnotation = escapedName !== name ? `@as("` + name + `") ` : "";
|
|
277
|
+
return ` ` + aliasAnnotation + escapedName + `: ` + finalType + `,`;
|
|
278
|
+
}).join("\n");
|
|
279
|
+
return `{\n` + fields$1 + `\n}`;
|
|
280
|
+
}
|
|
281
|
+
if (nonObjectTypes.length !== 0 && objectProps.length === 0) {
|
|
282
|
+
return recurse(Stdlib_Option.getOr(types$1[types$1.length - 1 | 0], "Unknown"));
|
|
283
|
+
}
|
|
284
|
+
GenerationContext.addWarning(ctx, {
|
|
285
|
+
TAG: "IntersectionNotFullySupported",
|
|
286
|
+
location: ctx.path,
|
|
287
|
+
note: "Mixed object/non-object intersection"
|
|
288
|
+
});
|
|
289
|
+
let fields$2 = objectProps.map(param => {
|
|
290
|
+
let name = param[0];
|
|
291
|
+
let typeCode = recurse(param[1]);
|
|
292
|
+
let finalType = param[2] ? typeCode : `option<` + typeCode + `>`;
|
|
293
|
+
let camelName = JsConvertCase.toCamelCase(name);
|
|
294
|
+
let escapedName = CodegenUtils.escapeKeyword(camelName);
|
|
295
|
+
let aliasAnnotation = escapedName !== name ? `@as("` + name + `") ` : "";
|
|
296
|
+
return ` ` + aliasAnnotation + escapedName + `: ` + finalType + `,`;
|
|
297
|
+
}).join("\n");
|
|
298
|
+
return `{\n` + fields$2 + `\n}`;
|
|
154
299
|
case "Reference" :
|
|
155
300
|
let ref = irType._0;
|
|
156
301
|
let available = ctx.availableSchemas;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@f3liz/rescript-autogen-openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Generate ReScript code with Sury schemas from OpenAPI 3.1 specs. Supports multiple forks with diff/merge capabilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rescript",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"build": "rescript && node scripts/generate-dts.mjs",
|
|
30
30
|
"clean": "rescript clean",
|
|
31
31
|
"watch": "rescript build -w",
|
|
32
|
-
"test": "node --test"
|
|
32
|
+
"test": "node --test",
|
|
33
|
+
"prepublishOnly": "npm run build && npm test"
|
|
33
34
|
},
|
|
34
35
|
"main": "lib/es6/src/Codegen.mjs",
|
|
35
36
|
"types": "lib/es6/src/Codegen.d.ts",
|
|
@@ -66,40 +66,56 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
66
66
|
| NullLiteral => "S.literal(null)"
|
|
67
67
|
}
|
|
68
68
|
| Union(types) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
},
|
|
69
|
+
// Separate Null from non-null members (handles OpenAPI 3.1 nullable via oneOf)
|
|
70
|
+
let nonNullTypes = types->Array.filter(t =>
|
|
71
|
+
switch t {
|
|
72
|
+
| Null | Literal(NullLiteral) => false
|
|
73
|
+
| _ => true
|
|
74
|
+
}
|
|
76
75
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`S.array(${recurse(Option.getOr(arrayItemType, Unknown))})`
|
|
83
|
-
} else if (
|
|
84
|
-
types->Array.every(t =>
|
|
85
|
-
switch t {
|
|
86
|
-
| Literal(StringLiteral(_)) => true
|
|
87
|
-
| _ => false
|
|
88
|
-
}
|
|
89
|
-
) &&
|
|
90
|
-
Array.length(types) > 0 &&
|
|
91
|
-
Array.length(types) <= 50
|
|
92
|
-
) {
|
|
93
|
-
`S.union([${types->Array.map(recurse)->Array.join(", ")}])`
|
|
76
|
+
let hasNull = Array.length(nonNullTypes) < Array.length(types)
|
|
77
|
+
|
|
78
|
+
// If the union is just [T, null], treat as nullable
|
|
79
|
+
if hasNull && Array.length(nonNullTypes) == 1 {
|
|
80
|
+
`S.nullableAsOption(${recurse(nonNullTypes->Array.getUnsafe(0))})`
|
|
94
81
|
} else {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
let effectiveTypes = hasNull ? nonNullTypes : types
|
|
83
|
+
|
|
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
|
+
},
|
|
101
91
|
)
|
|
102
|
-
|
|
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
|
+
`S.array(${recurse(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
|
+
`S.union([${effectiveTypes->Array.map(recurse)->Array.join(", ")}])`
|
|
111
|
+
} else if Array.length(effectiveTypes) > 0 {
|
|
112
|
+
// Generate S.union for mixed-type unions
|
|
113
|
+
`S.union([${effectiveTypes->Array.map(recurse)->Array.join(", ")}])`
|
|
114
|
+
} else {
|
|
115
|
+
"S.json"
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
hasNull ? `S.nullableAsOption(${result})` : result
|
|
103
119
|
}
|
|
104
120
|
| Intersection(types) =>
|
|
105
121
|
if types->Array.every(t =>
|
|
@@ -110,11 +126,47 @@ let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType
|
|
|
110
126
|
) && Array.length(types) > 0 {
|
|
111
127
|
recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
112
128
|
} else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
// Try to merge all Object types in the intersection
|
|
130
|
+
let (objectProps, nonObjectTypes) = types->Array.reduce(
|
|
131
|
+
([], []),
|
|
132
|
+
((props, nonObj), t) =>
|
|
133
|
+
switch t {
|
|
134
|
+
| Object({properties}) => (Array.concat(props, properties), nonObj)
|
|
135
|
+
| _ => (props, Array.concat(nonObj, [t]))
|
|
136
|
+
},
|
|
116
137
|
)
|
|
117
|
-
|
|
138
|
+
if Array.length(objectProps) > 0 && Array.length(nonObjectTypes) == 0 {
|
|
139
|
+
// All objects: merge properties into single S.object
|
|
140
|
+
let fields =
|
|
141
|
+
objectProps
|
|
142
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
143
|
+
let schemaCode = recurse(fieldType)
|
|
144
|
+
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
145
|
+
isRequired
|
|
146
|
+
? ` ${camelName}: s.field("${name}", ${schemaCode}),`
|
|
147
|
+
: ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
|
|
148
|
+
})
|
|
149
|
+
->Array.join("\n")
|
|
150
|
+
`S.object(s => {\n${fields}\n })`
|
|
151
|
+
} else if Array.length(nonObjectTypes) > 0 && Array.length(objectProps) == 0 {
|
|
152
|
+
recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
153
|
+
} else {
|
|
154
|
+
addWarning(
|
|
155
|
+
ctx,
|
|
156
|
+
IntersectionNotFullySupported({location: ctx.path, note: "Mixed object/non-object intersection"}),
|
|
157
|
+
)
|
|
158
|
+
let fields =
|
|
159
|
+
objectProps
|
|
160
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
161
|
+
let schemaCode = recurse(fieldType)
|
|
162
|
+
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
163
|
+
isRequired
|
|
164
|
+
? ` ${camelName}: s.field("${name}", ${schemaCode}),`
|
|
165
|
+
: ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
|
|
166
|
+
})
|
|
167
|
+
->Array.join("\n")
|
|
168
|
+
`S.object(s => {\n${fields}\n })`
|
|
169
|
+
}
|
|
118
170
|
}
|
|
119
171
|
| Reference(ref) =>
|
|
120
172
|
let schemaPath = switch ctx.availableSchemas {
|
|
@@ -48,54 +48,89 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
48
48
|
| NullLiteral => "unit"
|
|
49
49
|
}
|
|
50
50
|
| Union(types) =>
|
|
51
|
-
//
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| _ => (hArr, true, arrItem, Some(t))
|
|
58
|
-
},
|
|
51
|
+
// Separate Null from non-null members (handles OpenAPI 3.1 nullable via oneOf)
|
|
52
|
+
let nonNullTypes = types->Array.filter(t =>
|
|
53
|
+
switch t {
|
|
54
|
+
| Null | Literal(NullLiteral) => false
|
|
55
|
+
| _ => true
|
|
56
|
+
}
|
|
59
57
|
)
|
|
58
|
+
let hasNull = Array.length(nonNullTypes) < Array.length(types)
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
) {
|
|
66
|
-
`array<${recurse(Option.getOr(arrayItemType, Unknown))}>`
|
|
67
|
-
} else if (
|
|
68
|
-
types->Array.every(t =>
|
|
69
|
-
switch t {
|
|
70
|
-
| Literal(StringLiteral(_)) => true
|
|
71
|
-
| _ => false
|
|
72
|
-
}
|
|
73
|
-
) &&
|
|
74
|
-
Array.length(types) > 0 &&
|
|
75
|
-
Array.length(types) <= 50
|
|
76
|
-
) {
|
|
77
|
-
let variants =
|
|
78
|
-
types
|
|
79
|
-
->Array.map(t =>
|
|
80
|
-
switch t {
|
|
81
|
-
| Literal(StringLiteral(s)) => `#${CodegenUtils.toPascalCase(s)}`
|
|
82
|
-
| _ => "#Unknown"
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
->Array.join(" | ")
|
|
86
|
-
`[${variants}]`
|
|
60
|
+
// If the union is just [T, null], treat as option<T>
|
|
61
|
+
if hasNull && Array.length(nonNullTypes) == 1 {
|
|
62
|
+
let inner = recurse(nonNullTypes->Array.getUnsafe(0))
|
|
63
|
+
`option<${inner}>`
|
|
87
64
|
} else {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
65
|
+
// Work with the non-null types (re-wrap in option at the end if hasNull)
|
|
66
|
+
let effectiveTypes = hasNull ? nonNullTypes : types
|
|
67
|
+
|
|
68
|
+
// Attempt to simplify common union patterns
|
|
69
|
+
let (hasArray, hasNonArray, arrayItemType, nonArrayType) = effectiveTypes->Array.reduce(
|
|
70
|
+
(false, false, None, None),
|
|
71
|
+
((hArr, hNonArr, arrItem, nonArr), t) =>
|
|
72
|
+
switch t {
|
|
73
|
+
| Array({items}) => (true, hNonArr, Some(items), nonArr)
|
|
74
|
+
| _ => (hArr, true, arrItem, Some(t))
|
|
75
|
+
},
|
|
94
76
|
)
|
|
95
|
-
|
|
77
|
+
|
|
78
|
+
let result = if (
|
|
79
|
+
hasArray &&
|
|
80
|
+
hasNonArray &&
|
|
81
|
+
Array.length(effectiveTypes) == 2 &&
|
|
82
|
+
SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
|
|
83
|
+
) {
|
|
84
|
+
`array<${recurse(Option.getOr(arrayItemType, Unknown))}>`
|
|
85
|
+
} else if (
|
|
86
|
+
effectiveTypes->Array.every(t =>
|
|
87
|
+
switch t {
|
|
88
|
+
| Literal(StringLiteral(_)) => true
|
|
89
|
+
| _ => false
|
|
90
|
+
}
|
|
91
|
+
) &&
|
|
92
|
+
Array.length(effectiveTypes) > 0 &&
|
|
93
|
+
Array.length(effectiveTypes) <= 50
|
|
94
|
+
) {
|
|
95
|
+
let variants =
|
|
96
|
+
effectiveTypes
|
|
97
|
+
->Array.map(t =>
|
|
98
|
+
switch t {
|
|
99
|
+
| Literal(StringLiteral(s)) => `#${CodegenUtils.toPascalCase(s)}`
|
|
100
|
+
| _ => "#Unknown"
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
->Array.join(" | ")
|
|
104
|
+
`[${variants}]`
|
|
105
|
+
} else if Array.length(effectiveTypes) > 0 {
|
|
106
|
+
// Generate @unboxed variant for mixed-type unions
|
|
107
|
+
let hasPrimitives = ref(false)
|
|
108
|
+
let variantCases = effectiveTypes->Array.mapWithIndex((t, i) => {
|
|
109
|
+
let (tag, typeStr) = switch t {
|
|
110
|
+
| String(_) => { hasPrimitives := true; ("String", "string") }
|
|
111
|
+
| Number(_) => { hasPrimitives := true; ("Float", "float") }
|
|
112
|
+
| Integer(_) => { hasPrimitives := true; ("Int", "int") }
|
|
113
|
+
| Boolean => { hasPrimitives := true; ("Bool", "bool") }
|
|
114
|
+
| Array({items}) => ("Array", `array<${recurse(items)}>`)
|
|
115
|
+
| Object(_) => ("Object", recurse(t))
|
|
116
|
+
| Reference(ref) => {
|
|
117
|
+
let name = ref->String.split("/")->Array.get(ref->String.split("/")->Array.length - 1)->Option.getOr("")
|
|
118
|
+
(CodegenUtils.toPascalCase(name), recurse(t))
|
|
119
|
+
}
|
|
120
|
+
| _ => (`V${Int.toString(i)}`, recurse(t))
|
|
121
|
+
}
|
|
122
|
+
` | ${tag}(${typeStr})`
|
|
123
|
+
})
|
|
124
|
+
let unboxedAttr = hasPrimitives.contents ? "@unboxed " : ""
|
|
125
|
+
`${unboxedAttr}[\n${variantCases->Array.join("\n")}\n]`
|
|
126
|
+
} else {
|
|
127
|
+
"JSON.t"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
hasNull ? `option<${result}>` : result
|
|
96
131
|
}
|
|
97
132
|
| Intersection(types) =>
|
|
98
|
-
//
|
|
133
|
+
// Support for intersections: merge object properties or pick last reference
|
|
99
134
|
if types->Array.every(t =>
|
|
100
135
|
switch t {
|
|
101
136
|
| Reference(_) => true
|
|
@@ -104,11 +139,51 @@ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType:
|
|
|
104
139
|
) && Array.length(types) > 0 {
|
|
105
140
|
recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
106
141
|
} else {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
// Try to merge all Object types in the intersection
|
|
143
|
+
let (objectProps, nonObjectTypes) = types->Array.reduce(
|
|
144
|
+
([], []),
|
|
145
|
+
((props, nonObj), t) =>
|
|
146
|
+
switch t {
|
|
147
|
+
| Object({properties}) => (Array.concat(props, properties), nonObj)
|
|
148
|
+
| _ => (props, Array.concat(nonObj, [t]))
|
|
149
|
+
},
|
|
110
150
|
)
|
|
111
|
-
|
|
151
|
+
if Array.length(objectProps) > 0 && Array.length(nonObjectTypes) == 0 {
|
|
152
|
+
// All objects: merge properties
|
|
153
|
+
let fields =
|
|
154
|
+
objectProps
|
|
155
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
156
|
+
let typeCode = recurse(fieldType)
|
|
157
|
+
let finalType = isRequired ? typeCode : `option<${typeCode}>`
|
|
158
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
159
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
160
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
161
|
+
` ${aliasAnnotation}${escapedName}: ${finalType},`
|
|
162
|
+
})
|
|
163
|
+
->Array.join("\n")
|
|
164
|
+
`{\n${fields}\n}`
|
|
165
|
+
} else if Array.length(nonObjectTypes) > 0 && Array.length(objectProps) == 0 {
|
|
166
|
+
// No objects: pick last type as best effort
|
|
167
|
+
recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
168
|
+
} else {
|
|
169
|
+
addWarning(
|
|
170
|
+
ctx,
|
|
171
|
+
IntersectionNotFullySupported({location: ctx.path, note: "Mixed object/non-object intersection"}),
|
|
172
|
+
)
|
|
173
|
+
// Merge what we can, ignore non-object parts
|
|
174
|
+
let fields =
|
|
175
|
+
objectProps
|
|
176
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
177
|
+
let typeCode = recurse(fieldType)
|
|
178
|
+
let finalType = isRequired ? typeCode : `option<${typeCode}>`
|
|
179
|
+
let camelName = name->CodegenUtils.toCamelCase
|
|
180
|
+
let escapedName = camelName->CodegenUtils.escapeKeyword
|
|
181
|
+
let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
|
|
182
|
+
` ${aliasAnnotation}${escapedName}: ${finalType},`
|
|
183
|
+
})
|
|
184
|
+
->Array.join("\n")
|
|
185
|
+
`{\n${fields}\n}`
|
|
186
|
+
}
|
|
112
187
|
}
|
|
113
188
|
| Option(inner) => `option<${recurse(inner)}>`
|
|
114
189
|
| Reference(ref) =>
|