@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 match = Stdlib_Array.reduce(types, [
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
- if (match[0] && match[1] && SchemaIR.equals(Stdlib_Option.getOr(arrayItemType, "Unknown"), Stdlib_Option.getOr(match[3], "Unknown"))) {
126
- return `S.array(` + recurse(Stdlib_Option.getOr(arrayItemType, "Unknown")) + `)`;
127
- } else if (types.every(t => {
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
- if (t.TAG !== "Literal") {
132
- return false;
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
- let tmp = t._0;
135
- return typeof tmp !== "object" ? false : tmp.TAG === "StringLiteral";
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
- case "Intersection" :
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 match = Stdlib_Array.reduce(types, [
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
- if (match[0] && match[1] && SchemaIR.equals(Stdlib_Option.getOr(arrayItemType, "Unknown"), Stdlib_Option.getOr(match[3], "Unknown"))) {
104
- return `array<` + recurse(Stdlib_Option.getOr(arrayItemType, "Unknown")) + `>`;
105
- }
106
- if (types.every(t => {
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
- }) && types.length !== 0 && types.length <= 50) {
120
- let variants = types.map(t => {
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
- return `[` + variants + `]`;
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 => typeof t !== "object" ? false : t.TAG === "Reference") && types$1.length !== 0) {
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.1.7",
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
- let (hasArray, hasNonArray, arrayItemType, nonArrayType) = types->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
- },
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
- if (
78
- hasArray &&
79
- hasNonArray &&
80
- SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
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
- addWarning(
96
- ctx,
97
- ComplexUnionSimplified({
98
- location: ctx.path,
99
- types: types->Array.map(SchemaIR.toString)->Array.join(" | "),
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
- "S.json"
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
- addWarning(
114
- ctx,
115
- IntersectionNotFullySupported({location: ctx.path, note: "Complex intersection"}),
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
- "S.json"
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
- // Attempt to simplify common union patterns
52
- let (hasArray, hasNonArray, arrayItemType, nonArrayType) = types->Array.reduce(
53
- (false, false, None, None),
54
- ((hArr, hNonArr, arrItem, nonArr), t) =>
55
- switch t {
56
- | Array({items}) => (true, hNonArr, Some(items), nonArr)
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
- if (
62
- hasArray &&
63
- hasNonArray &&
64
- SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
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
- addWarning(
89
- ctx,
90
- ComplexUnionSimplified({
91
- location: ctx.path,
92
- types: types->Array.map(SchemaIR.toString)->Array.join(" | "),
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
- "JSON.t"
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
- // Basic support for intersections by picking the last reference or falling back
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
- addWarning(
108
- ctx,
109
- IntersectionNotFullySupported({location: ctx.path, note: "Complex intersection"}),
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
- "JSON.t"
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) =>