@f3liz/rescript-autogen-openapi 0.1.6 → 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.

Potentially problematic release.


This version of @f3liz/rescript-autogen-openapi might be problematic. Click here for more details.

@@ -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) =>
@@ -3,6 +3,7 @@
3
3
  // Error.res - Compact error and warning types with helpers
4
4
 
5
5
  // Error context for debugging (defined here to avoid circular dependency)
6
+ @genType
6
7
  type context = {
7
8
  path: string,
8
9
  operation: string,
@@ -10,6 +11,7 @@ type context = {
10
11
  }
11
12
 
12
13
  // Structured error types (keep original names for backward compat)
14
+ @genType
13
15
  type t =
14
16
  | SpecResolutionError({url: string, message: string})
15
17
  | SchemaParseError({context: context, reason: string})
@@ -22,6 +24,7 @@ type t =
22
24
 
23
25
  // Warning types
24
26
  module Warning = {
27
+ @genType
25
28
  type t =
26
29
  | FallbackToJson({reason: string, context: context})
27
30
  | UnsupportedFeature({feature: string, fallback: string, location: string})
@@ -2,20 +2,24 @@
2
2
 
3
3
  // Config.res - Generation configuration types
4
4
 
5
+ @genType
5
6
  type generationStrategy =
6
7
  | Separate
7
8
  | SharedBase
8
9
 
10
+ @genType
9
11
  type breakingChangeHandling =
10
12
  | Error
11
13
  | Warn
12
14
  | Ignore
13
15
 
16
+ @genType
14
17
  type forkSpecConfig = {
15
18
  name: string,
16
19
  specPath: string,
17
20
  }
18
21
 
22
+ @genType
19
23
  type generationTargets = {
20
24
  rescriptApi: bool, // Generate base ReScript API (always true by default)
21
25
  rescriptWrapper: bool, // Generate ReScript thin wrapper (pipe-first)
@@ -23,6 +27,7 @@ type generationTargets = {
23
27
  typescriptWrapper: bool, // Generate TypeScript/JavaScript wrapper
24
28
  }
25
29
 
30
+ @genType
26
31
  type t = {
27
32
  specPath: string,
28
33
  forkSpecs: option<array<forkSpecConfig>>,
@@ -43,6 +48,7 @@ type t = {
43
48
  }
44
49
 
45
50
  // Default configuration
51
+ @genType
46
52
  let make = (
47
53
  ~specPath,
48
54
  ~outputDir,