@f3liz/rescript-autogen-openapi 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +98 -0
  3. package/lib/es6/src/Codegen.mjs +423 -0
  4. package/lib/es6/src/Types.mjs +20 -0
  5. package/lib/es6/src/core/CodegenUtils.mjs +186 -0
  6. package/lib/es6/src/core/DocOverride.mjs +399 -0
  7. package/lib/es6/src/core/FileSystem.mjs +78 -0
  8. package/lib/es6/src/core/IRBuilder.mjs +201 -0
  9. package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
  10. package/lib/es6/src/core/Pipeline.mjs +150 -0
  11. package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
  12. package/lib/es6/src/core/Result.mjs +378 -0
  13. package/lib/es6/src/core/SchemaIR.mjs +355 -0
  14. package/lib/es6/src/core/SchemaIRParser.mjs +490 -0
  15. package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
  16. package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
  17. package/lib/es6/src/core/SpecDiffer.mjs +251 -0
  18. package/lib/es6/src/core/SpecMerger.mjs +237 -0
  19. package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +125 -0
  20. package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
  21. package/lib/es6/src/generators/EndpointGenerator.mjs +172 -0
  22. package/lib/es6/src/generators/IRToSuryGenerator.mjs +233 -0
  23. package/lib/es6/src/generators/IRToTypeGenerator.mjs +241 -0
  24. package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
  25. package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
  26. package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
  27. package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
  28. package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
  29. package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
  30. package/lib/es6/src/types/CodegenError.mjs +79 -0
  31. package/lib/es6/src/types/Config.mjs +42 -0
  32. package/lib/es6/src/types/GenerationContext.mjs +24 -0
  33. package/package.json +44 -0
  34. package/rescript.json +20 -0
  35. package/src/Codegen.res +222 -0
  36. package/src/Types.res +195 -0
  37. package/src/core/CodegenUtils.res +130 -0
  38. package/src/core/DocOverride.res +504 -0
  39. package/src/core/FileSystem.res +62 -0
  40. package/src/core/IRBuilder.res +66 -0
  41. package/src/core/OpenAPIParser.res +144 -0
  42. package/src/core/Pipeline.res +51 -0
  43. package/src/core/ReferenceResolver.res +41 -0
  44. package/src/core/Result.res +187 -0
  45. package/src/core/SchemaIR.res +258 -0
  46. package/src/core/SchemaIRParser.res +360 -0
  47. package/src/core/SchemaRefResolver.res +143 -0
  48. package/src/core/SchemaRegistry.res +107 -0
  49. package/src/core/SpecDiffer.res +270 -0
  50. package/src/core/SpecMerger.res +245 -0
  51. package/src/generators/ComponentSchemaGenerator.res +127 -0
  52. package/src/generators/DiffReportGenerator.res +152 -0
  53. package/src/generators/EndpointGenerator.res +172 -0
  54. package/src/generators/IRToSuryGenerator.res +199 -0
  55. package/src/generators/IRToTypeGenerator.res +199 -0
  56. package/src/generators/IRToTypeScriptGenerator.res +72 -0
  57. package/src/generators/ModuleGenerator.res +362 -0
  58. package/src/generators/SchemaCodeGenerator.res +83 -0
  59. package/src/generators/ThinWrapperGenerator.res +124 -0
  60. package/src/generators/TypeScriptDtsGenerator.res +193 -0
  61. package/src/generators/TypeScriptWrapperGenerator.res +166 -0
  62. package/src/types/CodegenError.res +82 -0
  63. package/src/types/Config.res +89 -0
  64. package/src/types/GenerationContext.res +23 -0
@@ -0,0 +1,144 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // OpenAPIParser.res - Parse OpenAPI 3.1 specs
4
+
5
+ // Parse HTTP method from string
6
+ let parseMethod = (methodStr: string): option<Types.httpMethod> => {
7
+ switch methodStr->String.toLowerCase {
8
+ | "get" => Some(#GET)
9
+ | "post" => Some(#POST)
10
+ | "put" => Some(#PUT)
11
+ | "patch" => Some(#PATCH)
12
+ | "delete" => Some(#DELETE)
13
+ | "head" => Some(#HEAD)
14
+ | "options" => Some(#OPTIONS)
15
+ | _ => None
16
+ }
17
+ }
18
+
19
+ // Convert httpMethod to string
20
+ let httpMethodToString = (method: Types.httpMethod): string => {
21
+ switch method {
22
+ | #GET => "get"
23
+ | #POST => "post"
24
+ | #PUT => "put"
25
+ | #PATCH => "patch"
26
+ | #DELETE => "delete"
27
+ | #HEAD => "head"
28
+ | #OPTIONS => "options"
29
+ }
30
+ }
31
+
32
+ // Convert tuple to endpoint
33
+ let tupleToEndpoint = ((path, method, operation): (string, Types.httpMethod, Types.operation)): Types.endpoint => {
34
+ {
35
+ path,
36
+ method: httpMethodToString(method),
37
+ operationId: operation.operationId,
38
+ summary: operation.summary,
39
+ description: operation.description,
40
+ tags: operation.tags,
41
+ requestBody: operation.requestBody,
42
+ responses: operation.responses,
43
+ parameters: operation.parameters,
44
+ }
45
+ }
46
+
47
+ // Extract operations from a path item
48
+ let getOperations = (path: string, {get, post, put, patch, delete, head, options}: Types.pathItem): array<(string, Types.httpMethod, Types.operation)> => {
49
+ [
50
+ (#GET, get),
51
+ (#POST, post),
52
+ (#PUT, put),
53
+ (#PATCH, patch),
54
+ (#DELETE, delete),
55
+ (#HEAD, head),
56
+ (#OPTIONS, options),
57
+ ]->Array.filterMap(((method, op)) => op->Option.map(op => (path, method, op)))
58
+ }
59
+
60
+ // Get all endpoints from the spec
61
+ let getAllEndpoints = (spec: Types.openAPISpec): array<Types.endpoint> => {
62
+ let pathsArray = spec.paths->Dict.toArray
63
+
64
+ pathsArray
65
+ ->Array.flatMap(((path, pathItem)) => getOperations(path, pathItem))
66
+ ->Array.map(tupleToEndpoint)
67
+ }
68
+
69
+ // Group endpoints by tag
70
+ let groupByTag = (
71
+ endpoints: array<Types.endpoint>
72
+ ): Dict.t<array<Types.endpoint>> => {
73
+ let grouped = Dict.make()
74
+
75
+ endpoints->Array.forEach(endpoint => {
76
+ let tags = endpoint.tags->Option.getOr(["default"])
77
+
78
+ tags->Array.forEach(tag => {
79
+ let existing = grouped->Dict.get(tag)->Option.getOr([])
80
+ existing->Array.push(endpoint)
81
+ grouped->Dict.set(tag, existing)
82
+ })
83
+ })
84
+
85
+ grouped
86
+ }
87
+
88
+ // Get all schemas from components
89
+ let getAllSchemas = (spec: Types.openAPISpec): Dict.t<Types.jsonSchema> => {
90
+ spec.components
91
+ ->Option.flatMap(c => c.schemas)
92
+ ->Option.getOr(Dict.make())
93
+ }
94
+
95
+ // Extract operation ID or generate one
96
+ let getOperationId = (path: string, method: Types.httpMethod, operation: Types.operation): string => {
97
+ operation.operationId->Option.getOr({
98
+ // Generate operation ID from path and method
99
+ let methodStr = switch method {
100
+ | #GET => "get"
101
+ | #POST => "post"
102
+ | #PUT => "put"
103
+ | #PATCH => "patch"
104
+ | #DELETE => "delete"
105
+ | #HEAD => "head"
106
+ | #OPTIONS => "options"
107
+ }
108
+
109
+ let pathParts = path
110
+ ->String.replaceAll("/", "_")
111
+ ->String.replaceAll("{", "")
112
+ ->String.replaceAll("}", "")
113
+ ->String.replaceAll("-", "_")
114
+
115
+ `${methodStr}${pathParts}`
116
+ })
117
+ }
118
+
119
+ // Filter endpoints by tags
120
+ let filterByTags = (
121
+ ~endpoints: array<Types.endpoint>,
122
+ ~includeTags: array<string>,
123
+ ~excludeTags: array<string>,
124
+ ): array<Types.endpoint> => {
125
+ endpoints->Array.filter(endpoint => {
126
+ let operationTags = endpoint.tags->Option.getOr([])
127
+
128
+ // Check include tags
129
+ let included = operationTags->Array.some(tag => includeTags->Array.includes(tag))
130
+
131
+ // Check exclude tags
132
+ let excluded = operationTags->Array.some(tag => excludeTags->Array.includes(tag))
133
+
134
+ included && !excluded
135
+ })
136
+ }
137
+
138
+ // Get unique tags from all endpoints
139
+ let getAllTags = (endpoints: array<Types.endpoint>): array<string> => {
140
+ endpoints
141
+ ->Array.flatMap(endpoint => endpoint.tags->Option.getOr([]))
142
+ ->Set.fromArray
143
+ ->Set.toArray
144
+ }
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // Pipeline.res - Compact data transformation pipeline
4
+ open Types
5
+
6
+ type t = {
7
+ files: array<FileSystem.fileToWrite>,
8
+ warnings: array<warning>,
9
+ }
10
+
11
+ let empty = {files: [], warnings: []}
12
+
13
+ // Combine two pipelines
14
+ let merge = (a, b) => {
15
+ files: Array.concat(a.files, b.files),
16
+ warnings: Array.concat(a.warnings, b.warnings),
17
+ }
18
+
19
+ // Pipe-first API
20
+ let combine = outputs => {
21
+ files: outputs->Array.flatMap(p => p.files),
22
+ warnings: outputs->Array.flatMap(p => p.warnings),
23
+ }
24
+
25
+ let addFile = (file, p) => {...p, files: Array.concat(p.files, [file])}
26
+ let addFiles = (files, p) => {...p, files: Array.concat(p.files, files)}
27
+ let addWarning = (warning, p) => {...p, warnings: Array.concat(p.warnings, [warning])}
28
+ let addWarnings = (warnings, p) => {...p, warnings: Array.concat(p.warnings, warnings)}
29
+
30
+ let mapFiles = (fn, p) => {...p, files: p.files->Array.map(fn)}
31
+ let filterWarnings = (pred, p) => {...p, warnings: p.warnings->Array.filter(pred)}
32
+
33
+ // Constructors
34
+ let make = (~files=[], ~warnings=[], ()) => {files, warnings}
35
+ let fromFile = file => make(~files=[file], ())
36
+ let fromFiles = files => make(~files, ())
37
+ let fromWarning = warning => make(~warnings=[warning], ())
38
+ let fromWarnings = warnings => make(~warnings, ())
39
+
40
+ // Accessors
41
+ let files = p => p.files
42
+ let warnings = p => p.warnings
43
+ let fileCount = p => Array.length(p.files)
44
+ let warningCount = p => Array.length(p.warnings)
45
+ let filePaths = p => p.files->Array.map(f => f.path)
46
+
47
+ // Legacy aliases for compatibility during migration
48
+ type generationOutput = t
49
+ let withWarnings = (p, w) => addWarnings(w, p)
50
+ let withFiles = (p, f) => addFiles(f, p)
51
+ let fromFilesAndWarnings = (files, warnings) => make(~files, ~warnings, ())
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // ReferenceResolver.res - Utilities for resolving component schema references
4
+
5
+ // Convert a reference like "#/components/schemas/User" to module path
6
+ // If insideComponentSchemas=true, returns "User.t" (relative)
7
+ // If insideComponentSchemas=false, returns "{prefix}ComponentSchemas.User.t" (fully qualified)
8
+ let refToTypePath = (~insideComponentSchemas=false, ~modulePrefix="", ref: string): option<string> => {
9
+ // Handle #/components/schemas/SchemaName format
10
+ let parts = ref->String.split("/")
11
+ switch parts->Array.get(parts->Array.length - 1) {
12
+ | None => None
13
+ | Some(schemaName) => {
14
+ let moduleName = CodegenUtils.toPascalCase(schemaName)
15
+ if insideComponentSchemas {
16
+ Some(`${moduleName}.t`)
17
+ } else {
18
+ Some(`${modulePrefix}ComponentSchemas.${moduleName}.t`)
19
+ }
20
+ }
21
+ }
22
+ }
23
+
24
+ // Convert a reference like "#/components/schemas/User" to schema path
25
+ // If insideComponentSchemas=true, returns "User.schema" (relative)
26
+ // If insideComponentSchemas=false, returns "{prefix}ComponentSchemas.User.schema" (fully qualified)
27
+ let refToSchemaPath = (~insideComponentSchemas=false, ~modulePrefix="", ref: string): option<string> => {
28
+ // Handle #/components/schemas/SchemaName format
29
+ let parts = ref->String.split("/")
30
+ switch parts->Array.get(parts->Array.length - 1) {
31
+ | None => None
32
+ | Some(schemaName) => {
33
+ let moduleName = CodegenUtils.toPascalCase(schemaName)
34
+ if insideComponentSchemas {
35
+ Some(`${moduleName}.schema`)
36
+ } else {
37
+ Some(`${modulePrefix}ComponentSchemas.${moduleName}.schema`)
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,187 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // Result.res - Railway-oriented programming helpers
4
+ // Compact utilities for elegant error handling with pipe-first style
5
+
6
+ // Re-export core Result for convenience
7
+ include Result
8
+
9
+ // Applicative-style combinators
10
+ let map2 = (ra, rb, f) =>
11
+ switch (ra, rb) {
12
+ | (Ok(a), Ok(b)) => Ok(f(a, b))
13
+ | (Error(e), _) => Error(e)
14
+ | (_, Error(e)) => Error(e)
15
+ }
16
+
17
+ let map3 = (ra, rb, rc, f) =>
18
+ switch (ra, rb, rc) {
19
+ | (Ok(a), Ok(b), Ok(c)) => Ok(f(a, b, c))
20
+ | (Error(e), _, _) => Error(e)
21
+ | (_, Error(e), _) => Error(e)
22
+ | (_, _, Error(e)) => Error(e)
23
+ }
24
+
25
+ let map4 = (ra, rb, rc, rd, f) =>
26
+ switch (ra, rb, rc, rd) {
27
+ | (Ok(a), Ok(b), Ok(c), Ok(d)) => Ok(f(a, b, c, d))
28
+ | (Error(e), _, _, _) => Error(e)
29
+ | (_, Error(e), _, _) => Error(e)
30
+ | (_, _, Error(e), _) => Error(e)
31
+ | (_, _, _, Error(e)) => Error(e)
32
+ }
33
+
34
+ // Collect array of results into result of array
35
+ let all = results => {
36
+ let acc = []
37
+ let error = ref(None)
38
+ let len = Array.length(results)
39
+
40
+ let rec loop = idx => {
41
+ if idx < len && Option.isNone(error.contents) {
42
+ switch Array.getUnsafe(results, idx) {
43
+ | Ok(v) => {
44
+ Array.push(acc, v)
45
+ loop(idx + 1)
46
+ }
47
+ | Error(e) => error := Some(e)
48
+ }
49
+ }
50
+ }
51
+
52
+ loop(0)
53
+
54
+ switch error.contents {
55
+ | Some(e) => Error(e)
56
+ | None => Ok(acc)
57
+ }
58
+ }
59
+
60
+ // Collect results, ignoring successful values (just check for errors)
61
+ let allUnit = results => {
62
+ let error = ref(None)
63
+ let len = Array.length(results)
64
+
65
+ let rec loop = idx => {
66
+ if idx < len && Option.isNone(error.contents) {
67
+ switch Array.getUnsafe(results, idx) {
68
+ | Ok(_) => loop(idx + 1)
69
+ | Error(e) => error := Some(e)
70
+ }
71
+ }
72
+ }
73
+
74
+ loop(0)
75
+
76
+ switch error.contents {
77
+ | Some(e) => Error(e)
78
+ | None => Ok()
79
+ }
80
+ }
81
+
82
+ // Partition results into successes and errors
83
+ let partition = results => {
84
+ let successes = []
85
+ let errors = []
86
+ results->Array.forEach(r =>
87
+ switch r {
88
+ | Ok(v) => successes->Array.push(v)
89
+ | Error(e) => errors->Array.push(e)
90
+ }
91
+ )
92
+ (successes, errors)
93
+ }
94
+
95
+ // Try multiple computations, return first success or all errors
96
+ let firstSuccess = results => {
97
+ let errors = []
98
+ let success = ref(None)
99
+ let len = Array.length(results)
100
+
101
+ let rec loop = idx => {
102
+ if idx < len && Option.isNone(success.contents) {
103
+ switch Array.getUnsafe(results, idx) {
104
+ | Ok(v) => success := Some(v)
105
+ | Error(e) => {
106
+ Array.push(errors, e)
107
+ loop(idx + 1)
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ loop(0)
114
+
115
+ switch success.contents {
116
+ | Some(v) => Ok(v)
117
+ | None => Error(errors)
118
+ }
119
+ }
120
+
121
+ // Fold with early exit on error
122
+ let foldM = (arr, init, f) => {
123
+ let rec loop = (acc, idx) =>
124
+ if idx >= Array.length(arr) {
125
+ Ok(acc)
126
+ } else {
127
+ f(acc, arr->Array.getUnsafe(idx))->flatMap(newAcc => loop(newAcc, idx + 1))
128
+ }
129
+ loop(init, 0)
130
+ }
131
+
132
+ // Tap for side effects (useful for logging)
133
+ let tap = (result, f) => {
134
+ switch result {
135
+ | Ok(v) => f(v)
136
+ | Error(_) => ()
137
+ }
138
+ result
139
+ }
140
+
141
+ let tapError = (result, f) => {
142
+ switch result {
143
+ | Error(e) => f(e)
144
+ | Ok(_) => ()
145
+ }
146
+ result
147
+ }
148
+
149
+ // Convert option to result
150
+ let fromOption = (opt, error) =>
151
+ switch opt {
152
+ | Some(v) => Ok(v)
153
+ | None => Error(error)
154
+ }
155
+
156
+ // Convert to option (discarding error)
157
+ let toOption = result =>
158
+ switch result {
159
+ | Ok(v) => Some(v)
160
+ | Error(_) => None
161
+ }
162
+
163
+ // Recover from error with a function
164
+ let recover = (result, f) =>
165
+ switch result {
166
+ | Ok(_) as ok => ok
167
+ | Error(e) => f(e)
168
+ }
169
+
170
+ // Pipe-first versions of common operations
171
+ let getOr = (result, default) =>
172
+ switch result {
173
+ | Ok(v) => v
174
+ | Error(_) => default
175
+ }
176
+
177
+ let getExn = result =>
178
+ switch result {
179
+ | Ok(v) => v
180
+ | Error(_) => panic("Result.getExn called on Error")
181
+ }
182
+
183
+ let getError = result =>
184
+ switch result {
185
+ | Ok(_) => None
186
+ | Error(e) => Some(e)
187
+ }
@@ -0,0 +1,258 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // SchemaIR.res - Unified Intermediate Representation for JSON Schema
4
+ // This IR abstracts away JSON Schema details and provides a clean representation
5
+ // that can be used to generate both ReScript types and Sury schemas
6
+
7
+ // Validation constraints
8
+ type stringConstraints = {
9
+ minLength: option<int>,
10
+ maxLength: option<int>,
11
+ pattern: option<string>,
12
+ }
13
+
14
+ type numberConstraints = {
15
+ minimum: option<float>,
16
+ maximum: option<float>,
17
+ multipleOf: option<float>,
18
+ }
19
+
20
+ type arrayConstraints = {
21
+ minItems: option<int>,
22
+ maxItems: option<int>,
23
+ uniqueItems: bool,
24
+ }
25
+
26
+ // Core IR types
27
+ type rec irType =
28
+ | String({constraints: stringConstraints})
29
+ | Number({constraints: numberConstraints})
30
+ | Integer({constraints: numberConstraints})
31
+ | Boolean
32
+ | Null
33
+ | Array({items: irType, constraints: arrayConstraints})
34
+ | Object({
35
+ properties: array<(string, irType, bool)>, // (name, type, required)
36
+ additionalProperties: option<irType>,
37
+ })
38
+ | Literal(literalValue)
39
+ | Union(array<irType>)
40
+ | Intersection(array<irType>)
41
+ | Reference(string) // Schema reference like "#/components/schemas/User"
42
+ | Option(irType) // Nullable/optional types
43
+ | Unknown
44
+
45
+ and literalValue =
46
+ | StringLiteral(string)
47
+ | NumberLiteral(float)
48
+ | BooleanLiteral(bool)
49
+ | NullLiteral
50
+
51
+ // Named schema definition
52
+ type namedSchema = {
53
+ name: string,
54
+ description: option<string>,
55
+ type_: irType,
56
+ }
57
+
58
+ // Schema context for resolving references
59
+ type schemaContext = {
60
+ schemas: Dict.t<namedSchema>,
61
+ }
62
+
63
+ // Helpers
64
+ let isOptional = (irType: irType): bool => {
65
+ switch irType {
66
+ | Option(_) => true
67
+ | _ => false
68
+ }
69
+ }
70
+
71
+ let unwrapOption = (irType: irType): irType => {
72
+ switch irType {
73
+ | Option(inner) => inner
74
+ | other => other
75
+ }
76
+ }
77
+
78
+ let makeOptional = (irType: irType): irType => {
79
+ switch irType {
80
+ | Option(_) => irType // Already optional
81
+ | other => Option(other)
82
+ }
83
+ }
84
+
85
+ // Check if a type is simple (no complex nested structures)
86
+ let rec isSimpleType = (irType: irType): bool => {
87
+ switch irType {
88
+ | String(_) | Number(_) | Integer(_) | Boolean | Null | Reference(_) => true
89
+ | Option(inner) => isSimpleType(inner)
90
+ | Literal(_) => true
91
+ | Array({items, _}) => isSimpleType(items)
92
+ | Object(_) | Union(_) | Intersection(_) | Unknown => false
93
+ }
94
+ }
95
+
96
+ // Count the complexity of a type (for deciding whether to inline or extract)
97
+ let rec complexityScore = (irType: irType): int => {
98
+ switch irType {
99
+ | String(_) | Number(_) | Integer(_) | Boolean | Null | Reference(_) | Literal(_) => 1
100
+ | Option(inner) => complexityScore(inner)
101
+ | Array({items, _}) => 1 + complexityScore(items)
102
+ | Object({properties, _}) => {
103
+ let propsScore = properties
104
+ ->Array.map(((_, type_, _)) => complexityScore(type_))
105
+ ->Array.reduce(0, (acc, score) => acc + score)
106
+ 5 + propsScore // Objects are inherently complex
107
+ }
108
+ | Union(types) => {
109
+ let typesScore = types
110
+ ->Array.map(complexityScore)
111
+ ->Array.reduce(0, (acc, score) => acc + score)
112
+ 2 + typesScore
113
+ }
114
+ | Intersection(types) => {
115
+ let typesScore = types
116
+ ->Array.map(complexityScore)
117
+ ->Array.reduce(0, (acc, score) => acc + score)
118
+ 3 + typesScore
119
+ }
120
+ | Unknown => 1
121
+ }
122
+ }
123
+
124
+ // Extract nested complex types that should be named separately
125
+ // Returns (simplified type, extracted schemas)
126
+ let rec extractComplexTypes = (
127
+ ~baseName: string,
128
+ ~irType: irType,
129
+ ~threshold: int=10,
130
+ ): (irType, array<namedSchema>) => {
131
+ let score = complexityScore(irType)
132
+
133
+ if score <= threshold {
134
+ (irType, [])
135
+ } else {
136
+ switch irType {
137
+ | Object({properties, additionalProperties}) => {
138
+ // Extract complex property types
139
+ let (newProperties, allExtracted) = properties
140
+ ->Array.map(((propName, propType, required)) => {
141
+ let propBaseName = `${baseName}_${propName}`
142
+ let (newType, extracted) = extractComplexTypes(
143
+ ~baseName=propBaseName,
144
+ ~irType=propType,
145
+ ~threshold,
146
+ )
147
+ ((propName, newType, required), extracted)
148
+ })
149
+ ->Array.reduce(([], []), ((props, allExtr), ((prop, extracted))) => {
150
+ (Array.concat(props, [prop]), Array.concat(allExtr, extracted))
151
+ })
152
+
153
+ let newType = Object({
154
+ properties: newProperties,
155
+ additionalProperties,
156
+ })
157
+
158
+ (Reference(`#/components/schemas/${baseName}`), Array.concat(allExtracted, [{
159
+ name: baseName,
160
+ description: None,
161
+ type_: newType,
162
+ }]))
163
+ }
164
+ | Array({items, constraints}) => {
165
+ let (newItems, extracted) = extractComplexTypes(
166
+ ~baseName=`${baseName}_Item`,
167
+ ~irType=items,
168
+ ~threshold,
169
+ )
170
+ (Array({items: newItems, constraints}), extracted)
171
+ }
172
+ | Union(types) => {
173
+ let (newTypes, allExtracted) = types
174
+ ->Array.mapWithIndex((type_, i) => {
175
+ extractComplexTypes(
176
+ ~baseName=`${baseName}_Variant${Int.toString(i)}`,
177
+ ~irType=type_,
178
+ ~threshold,
179
+ )
180
+ })
181
+ ->Array.reduce(([], []), ((types, allExtr), ((type_, extracted))) => {
182
+ (Array.concat(types, [type_]), Array.concat(allExtr, extracted))
183
+ })
184
+ (Union(newTypes), allExtracted)
185
+ }
186
+ | Option(inner) => {
187
+ let (newInner, extracted) = extractComplexTypes(
188
+ ~baseName,
189
+ ~irType=inner,
190
+ ~threshold,
191
+ )
192
+ (Option(newInner), extracted)
193
+ }
194
+ | other => (other, [])
195
+ }
196
+ }
197
+ }
198
+
199
+ // Check if two IR types are equal (shallow comparison for T | Array<T> detection)
200
+ let rec equals = (a: irType, b: irType): bool => {
201
+ switch (a, b) {
202
+ | (String(_), String(_)) => true
203
+ | (Number(_), Number(_)) => true
204
+ | (Integer(_), Integer(_)) => true
205
+ | (Boolean, Boolean) => true
206
+ | (Null, Null) => true
207
+ | (Array({items: itemsA, _}), Array({items: itemsB, _})) => equals(itemsA, itemsB)
208
+ | (Reference(refA), Reference(refB)) => refA == refB
209
+ | (Option(innerA), Option(innerB)) => equals(innerA, innerB)
210
+ | (Literal(litA), Literal(litB)) => {
211
+ switch (litA, litB) {
212
+ | (StringLiteral(a), StringLiteral(b)) => a == b
213
+ | (NumberLiteral(a), NumberLiteral(b)) => a == b
214
+ | (BooleanLiteral(a), BooleanLiteral(b)) => a == b
215
+ | (NullLiteral, NullLiteral) => true
216
+ | _ => false
217
+ }
218
+ }
219
+ | (Unknown, Unknown) => true
220
+ | _ => false
221
+ }
222
+ }
223
+
224
+ // Pretty print IR type for debugging
225
+ let rec toString = (irType: irType): string => {
226
+ switch irType {
227
+ | String(_) => "String"
228
+ | Number(_) => "Number"
229
+ | Integer(_) => "Integer"
230
+ | Boolean => "Boolean"
231
+ | Null => "Null"
232
+ | Array({items, _}) => `Array<${toString(items)}>`
233
+ | Object({properties, _}) => {
234
+ let props = properties
235
+ ->Array.map(((name, type_, required)) => {
236
+ let req = required ? "" : "?"
237
+ `${name}${req}: ${toString(type_)}`
238
+ })
239
+ ->Array.join(", ")
240
+ `{ ${props} }`
241
+ }
242
+ | Literal(StringLiteral(s)) => `"${s}"`
243
+ | Literal(NumberLiteral(n)) => Float.toString(n)
244
+ | Literal(BooleanLiteral(b)) => b ? "true" : "false"
245
+ | Literal(NullLiteral) => "null"
246
+ | Union(types) => {
247
+ let typeStrs = types->Array.map(toString)->Array.join(" | ")
248
+ `(${typeStrs})`
249
+ }
250
+ | Intersection(types) => {
251
+ let typeStrs = types->Array.map(toString)->Array.join(" & ")
252
+ `(${typeStrs})`
253
+ }
254
+ | Reference(ref) => ref
255
+ | Option(inner) => `Option<${toString(inner)}>`
256
+ | Unknown => "Unknown"
257
+ }
258
+ }