@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.
- package/README.md +15 -2
- package/lib/es6/src/Codegen.d.ts +28 -0
- package/lib/es6/src/Types.d.ts +286 -0
- package/lib/es6/src/core/FileSystem.d.ts +4 -0
- package/lib/es6/src/core/Pipeline.d.ts +6 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +100 -27
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +167 -22
- package/lib/es6/src/types/CodegenError.d.ts +66 -0
- package/lib/es6/src/types/Config.d.ts +31 -0
- package/package.json +13 -8
- package/rescript.json +6 -0
- package/src/Codegen.res +9 -0
- package/src/Types.res +27 -0
- package/src/core/FileSystem.res +1 -0
- package/src/core/Pipeline.res +1 -0
- package/src/generators/IRToSuryGenerator.res +87 -35
- package/src/generators/IRToTypeGenerator.res +121 -46
- package/src/types/CodegenError.res +3 -0
- package/src/types/Config.res +6 -0
|
@@ -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) =>
|
|
@@ -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})
|
package/src/types/Config.res
CHANGED
|
@@ -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,
|