@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.
- package/LICENSE +339 -0
- package/README.md +98 -0
- package/lib/es6/src/Codegen.mjs +423 -0
- package/lib/es6/src/Types.mjs +20 -0
- package/lib/es6/src/core/CodegenUtils.mjs +186 -0
- package/lib/es6/src/core/DocOverride.mjs +399 -0
- package/lib/es6/src/core/FileSystem.mjs +78 -0
- package/lib/es6/src/core/IRBuilder.mjs +201 -0
- package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
- package/lib/es6/src/core/Pipeline.mjs +150 -0
- package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
- package/lib/es6/src/core/Result.mjs +378 -0
- package/lib/es6/src/core/SchemaIR.mjs +355 -0
- package/lib/es6/src/core/SchemaIRParser.mjs +490 -0
- package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
- package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
- package/lib/es6/src/core/SpecDiffer.mjs +251 -0
- package/lib/es6/src/core/SpecMerger.mjs +237 -0
- package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +125 -0
- package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
- package/lib/es6/src/generators/EndpointGenerator.mjs +172 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +233 -0
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +241 -0
- package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
- package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
- package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
- package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
- package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
- package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
- package/lib/es6/src/types/CodegenError.mjs +79 -0
- package/lib/es6/src/types/Config.mjs +42 -0
- package/lib/es6/src/types/GenerationContext.mjs +24 -0
- package/package.json +44 -0
- package/rescript.json +20 -0
- package/src/Codegen.res +222 -0
- package/src/Types.res +195 -0
- package/src/core/CodegenUtils.res +130 -0
- package/src/core/DocOverride.res +504 -0
- package/src/core/FileSystem.res +62 -0
- package/src/core/IRBuilder.res +66 -0
- package/src/core/OpenAPIParser.res +144 -0
- package/src/core/Pipeline.res +51 -0
- package/src/core/ReferenceResolver.res +41 -0
- package/src/core/Result.res +187 -0
- package/src/core/SchemaIR.res +258 -0
- package/src/core/SchemaIRParser.res +360 -0
- package/src/core/SchemaRefResolver.res +143 -0
- package/src/core/SchemaRegistry.res +107 -0
- package/src/core/SpecDiffer.res +270 -0
- package/src/core/SpecMerger.res +245 -0
- package/src/generators/ComponentSchemaGenerator.res +127 -0
- package/src/generators/DiffReportGenerator.res +152 -0
- package/src/generators/EndpointGenerator.res +172 -0
- package/src/generators/IRToSuryGenerator.res +199 -0
- package/src/generators/IRToTypeGenerator.res +199 -0
- package/src/generators/IRToTypeScriptGenerator.res +72 -0
- package/src/generators/ModuleGenerator.res +362 -0
- package/src/generators/SchemaCodeGenerator.res +83 -0
- package/src/generators/ThinWrapperGenerator.res +124 -0
- package/src/generators/TypeScriptDtsGenerator.res +193 -0
- package/src/generators/TypeScriptWrapperGenerator.res +166 -0
- package/src/types/CodegenError.res +82 -0
- package/src/types/Config.res +89 -0
- 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
|
+
}
|