@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
package/rescript.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@f3liz/rescript-autogen-openapi",
|
|
3
|
+
"sources": [
|
|
4
|
+
{
|
|
5
|
+
"dir": "src",
|
|
6
|
+
"subdirs": true
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"package-specs": [
|
|
10
|
+
{
|
|
11
|
+
"module": "esmodule",
|
|
12
|
+
"in-source": false
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"suffix": ".mjs",
|
|
16
|
+
"dependencies": [],
|
|
17
|
+
"warnings": {
|
|
18
|
+
"number": "-44-102"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/Codegen.res
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// Codegen.res - Main code generation orchestrator (DOP refactored)
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
// Promise bindings
|
|
7
|
+
@val external promiseAll: array<promise<'a>> => promise<array<'a>> = "Promise.all"
|
|
8
|
+
|
|
9
|
+
// Generate code from a single spec (pure - returns data)
|
|
10
|
+
let generateSingleSpecPure = (~spec: openAPISpec, ~config: generationConfig): result<Pipeline.t, codegenError> => {
|
|
11
|
+
try {
|
|
12
|
+
let targets = config.targets->Option.getOr(Config.defaultTargets())
|
|
13
|
+
let allEndpoints = OpenAPIParser.getAllEndpoints(spec)
|
|
14
|
+
let endpoints = switch config.includeTags {
|
|
15
|
+
| None => allEndpoints
|
|
16
|
+
| Some(includeTags) => OpenAPIParser.filterByTags(~endpoints=allEndpoints, ~includeTags, ~excludeTags=config.excludeTags->Option.getOr([]))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let baseOutput = targets.rescriptApi
|
|
20
|
+
? Pipeline.combine([
|
|
21
|
+
ComponentSchemaGenerator.generate(~spec, ~outputDir=config.outputDir),
|
|
22
|
+
config.modulePerTag
|
|
23
|
+
? ModuleGenerator.generateTagModuleFiles(
|
|
24
|
+
~endpoints,
|
|
25
|
+
~outputDir=config.outputDir,
|
|
26
|
+
~overrideDir=?config.docOverrideDir,
|
|
27
|
+
)
|
|
28
|
+
: ModuleGenerator.generateFlatModuleFile(
|
|
29
|
+
~moduleName="API",
|
|
30
|
+
~endpoints,
|
|
31
|
+
~outputDir=config.outputDir,
|
|
32
|
+
~overrideDir=?config.docOverrideDir,
|
|
33
|
+
),
|
|
34
|
+
])
|
|
35
|
+
: Pipeline.empty
|
|
36
|
+
|
|
37
|
+
let wrapperOutput = targets.rescriptWrapper
|
|
38
|
+
? ThinWrapperGenerator.generateWrapper(~spec, ~endpoints, ~outputDir=config.outputDir, ~wrapperModuleName=CodegenUtils.toPascalCase(spec.info.title) ++ "Wrapper", ~generatedModulePrefix="")
|
|
39
|
+
: Pipeline.empty
|
|
40
|
+
|
|
41
|
+
let dtsOutput = targets.typescriptDts
|
|
42
|
+
? TypeScriptDtsGenerator.generate(~spec, ~endpoints, ~outputDir=config.dtsOutputDir->Option.getOr(config.outputDir))
|
|
43
|
+
: Pipeline.empty
|
|
44
|
+
|
|
45
|
+
let tsWrapperOutput = targets.typescriptWrapper
|
|
46
|
+
? TypeScriptWrapperGenerator.generate(~endpoints, ~outputDir=config.wrapperOutputDir->Option.getOr(config.outputDir), ~generatedModulePath="../generated")
|
|
47
|
+
: Pipeline.empty
|
|
48
|
+
|
|
49
|
+
Result.Ok(Pipeline.combine([baseOutput, wrapperOutput, dtsOutput, tsWrapperOutput]))
|
|
50
|
+
} catch {
|
|
51
|
+
| JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
|
|
52
|
+
| _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Generate code from a single spec (with side effects)
|
|
57
|
+
let generateSingleSpec = async (~spec: openAPISpec, ~config: generationConfig): generationResult => {
|
|
58
|
+
switch generateSingleSpecPure(~spec, ~config) {
|
|
59
|
+
| Result.Error(err) => Result.Error(err)
|
|
60
|
+
| Result.Ok(output) =>
|
|
61
|
+
switch FileSystem.writeFiles(output.files) {
|
|
62
|
+
| Result.Error(errors) => Result.Error(UnknownError({message: `Failed to write files: ${Array.join(errors, ", ")}`, context: None}))
|
|
63
|
+
| Result.Ok(filePaths) =>
|
|
64
|
+
let overrideFiles = switch config.generateDocOverrides {
|
|
65
|
+
| Some(true) =>
|
|
66
|
+
let allEndpoints = OpenAPIParser.getAllEndpoints(spec)
|
|
67
|
+
let endpoints = switch config.includeTags {
|
|
68
|
+
| None => allEndpoints
|
|
69
|
+
| Some(includeTags) => OpenAPIParser.filterByTags(~endpoints=allEndpoints, ~includeTags, ~excludeTags=config.excludeTags->Option.getOr([]))
|
|
70
|
+
}
|
|
71
|
+
let files = DocOverride.generateOverrideFiles(~spec, ~endpoints, ~outputDir=config.docOverrideDir->Option.getOr("./docs"), ~host=spec.info.title, ~groupByTag=config.modulePerTag, ())
|
|
72
|
+
FileSystem.writeFiles(files)->Result.getOr([])
|
|
73
|
+
| _ => []
|
|
74
|
+
}
|
|
75
|
+
Result.Ok({generatedFiles: Array.concat(filePaths, overrideFiles), diff: None, warnings: output.warnings})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Process a single fork (pure - returns data)
|
|
81
|
+
let processForkPure = (~baseSpec: openAPISpec, ~baseEndpoints: array<endpoint>, ~fork: forkSpec, ~config: generationConfig): result<Pipeline.t, codegenError> => {
|
|
82
|
+
try {
|
|
83
|
+
let forkEndpoints = OpenAPIParser.getAllEndpoints(fork.spec)
|
|
84
|
+
let diff = SpecDiffer.generateDiff(~baseSpec, ~forkSpec=fork.spec, ~baseEndpoints, ~forkEndpoints)
|
|
85
|
+
|
|
86
|
+
let diffReportFile: option<FileSystem.fileToWrite> = config.generateDiffReport
|
|
87
|
+
? Some({path: FileSystem.makePath(config.outputDir, `${fork.name}-diff.md`), content: DiffReportGenerator.generateMarkdownReport(~diff, ~baseName="base", ~forkName=fork.name)})
|
|
88
|
+
: None
|
|
89
|
+
|
|
90
|
+
let (sharedSpec, extensionsSpec) = SpecMerger.mergeSpecs(~baseSpec, ~forkSpec=fork.spec, ~baseEndpoints, ~forkEndpoints, ~strategy=config.strategy)
|
|
91
|
+
let sharedEndpoints = OpenAPIParser.getAllEndpoints(sharedSpec)
|
|
92
|
+
let extensionEndpoints = OpenAPIParser.getAllEndpoints(extensionsSpec)
|
|
93
|
+
|
|
94
|
+
let mergeReportFile: FileSystem.fileToWrite = {
|
|
95
|
+
path: FileSystem.makePath(config.outputDir, `${fork.name}-merge.md`),
|
|
96
|
+
content: DiffReportGenerator.generateMergeReport(~stats=SpecMerger.getMergeStats(~baseEndpoints, ~forkEndpoints, ~baseSchemas=baseSpec.components->Option.flatMap(c => c.schemas), ~forkSchemas=fork.spec.components->Option.flatMap(c => c.schemas)), ~baseName="base", ~forkName=fork.name)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let codeOutput = switch config.strategy {
|
|
100
|
+
| Separate =>
|
|
101
|
+
Pipeline.fromFile({
|
|
102
|
+
path: FileSystem.makePath(config.outputDir, `${fork.name}.res`),
|
|
103
|
+
content: ModuleGenerator.generateFlatModuleCode(
|
|
104
|
+
~moduleName=CodegenUtils.toPascalCase(fork.name),
|
|
105
|
+
~endpoints=forkEndpoints,
|
|
106
|
+
~overrideDir=?config.docOverrideDir,
|
|
107
|
+
),
|
|
108
|
+
})
|
|
109
|
+
| SharedBase =>
|
|
110
|
+
let baseName = config.baseInstanceName->Option.getOrThrow(~message="baseInstanceName required")
|
|
111
|
+
let basePrefix = config.baseModulePrefix->Option.getOr(CodegenUtils.toPascalCase(baseName))
|
|
112
|
+
ModuleGenerator.generateSeparatePerTagModules(~baseName, ~basePrefix, ~forkName=fork.name, ~sharedEndpoints, ~extensionEndpoints, ~sharedSchemas=sharedSpec.components->Option.flatMap(c => c.schemas), ~extensionSchemas=fork.spec.components->Option.flatMap(c => c.schemas), ~outputDir=config.outputDir, ~overrideDir=?config.docOverrideDir)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let targets = config.targets->Option.getOr({rescriptApi: true, rescriptWrapper: false, typescriptDts: false, typescriptWrapper: false})
|
|
116
|
+
let (wSpec, wShared, wExt, wBasePrefix) = switch config.strategy {
|
|
117
|
+
| Separate => (fork.spec, forkEndpoints, [], "")
|
|
118
|
+
| SharedBase => (sharedSpec, sharedEndpoints, extensionEndpoints, config.baseModulePrefix->Option.getOr(config.baseInstanceName->Option.map(CodegenUtils.toPascalCase)->Option.getOr("")))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let wrapperOutput = targets.rescriptWrapper
|
|
122
|
+
? ThinWrapperGenerator.generateWrapper(~spec=wSpec, ~endpoints=wShared, ~extensionEndpoints=wExt, ~outputDir=FileSystem.makePath(config.outputDir, fork.name), ~wrapperModuleName=CodegenUtils.toPascalCase(fork.name) ++ "Wrapper", ~generatedModulePrefix=CodegenUtils.toPascalCase(fork.name), ~baseModulePrefix=wBasePrefix)
|
|
123
|
+
: Pipeline.empty
|
|
124
|
+
|
|
125
|
+
let allWEndpoints = Array.concat(wShared, wExt)
|
|
126
|
+
let dtsOutput = targets.typescriptDts
|
|
127
|
+
? TypeScriptDtsGenerator.generate(~spec=wSpec, ~endpoints=allWEndpoints, ~outputDir=FileSystem.makePath(config.dtsOutputDir->Option.getOr(config.outputDir), fork.name))
|
|
128
|
+
: Pipeline.empty
|
|
129
|
+
|
|
130
|
+
let tsWrapperOutput = targets.typescriptWrapper
|
|
131
|
+
? TypeScriptWrapperGenerator.generate(~endpoints=allWEndpoints, ~outputDir=FileSystem.makePath(config.wrapperOutputDir->Option.getOr(config.outputDir), fork.name), ~generatedModulePath=`../../generated/${fork.name}`)
|
|
132
|
+
: Pipeline.empty
|
|
133
|
+
|
|
134
|
+
let reports = Pipeline.fromFiles([mergeReportFile, ...diffReportFile->Option.map(f => [f])->Option.getOr([])])
|
|
135
|
+
Result.Ok(Pipeline.combine([reports, codeOutput, wrapperOutput, dtsOutput, tsWrapperOutput]))
|
|
136
|
+
} catch {
|
|
137
|
+
| JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
|
|
138
|
+
| _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Generate code from multiple specs (pure - returns data)
|
|
143
|
+
let generateMultiSpecPure = (~baseSpec: openAPISpec, ~forkSpecs: array<forkSpec>, ~config: generationConfig): result<Pipeline.t, codegenError> => {
|
|
144
|
+
try {
|
|
145
|
+
let baseEndpoints = OpenAPIParser.getAllEndpoints(baseSpec)
|
|
146
|
+
let forkResults = forkSpecs->Array.map(fork => processForkPure(~baseSpec, ~baseEndpoints, ~fork, ~config))
|
|
147
|
+
|
|
148
|
+
switch forkResults->Array.find(Result.isError) {
|
|
149
|
+
| Some(Result.Error(err)) => Result.Error(err)
|
|
150
|
+
| _ =>
|
|
151
|
+
let outputs = forkResults->Array.filterMap(res => switch res { | Ok(v) => Some(v) | Error(_) => None })
|
|
152
|
+
let targets = config.targets->Option.getOr(Config.defaultTargets())
|
|
153
|
+
let baseName = config.baseInstanceName->Option.getOrThrow(~message="baseInstanceName required")
|
|
154
|
+
let basePrefix = config.baseModulePrefix->Option.getOr(CodegenUtils.toPascalCase(baseName))
|
|
155
|
+
let baseOutputDir = FileSystem.makePath(config.outputDir, baseName)
|
|
156
|
+
|
|
157
|
+
let baseWrappers = Pipeline.combine([
|
|
158
|
+
targets.rescriptWrapper ? ThinWrapperGenerator.generateWrapper(~spec=baseSpec, ~endpoints=baseEndpoints, ~outputDir=baseOutputDir, ~wrapperModuleName=basePrefix ++ "Wrapper", ~generatedModulePrefix=basePrefix) : Pipeline.empty,
|
|
159
|
+
targets.typescriptDts ? TypeScriptDtsGenerator.generate(~spec=baseSpec, ~endpoints=baseEndpoints, ~outputDir=FileSystem.makePath(config.dtsOutputDir->Option.getOr(config.outputDir), baseName)) : Pipeline.empty,
|
|
160
|
+
targets.typescriptWrapper ? TypeScriptWrapperGenerator.generate(~endpoints=baseEndpoints, ~outputDir=FileSystem.makePath(config.wrapperOutputDir->Option.getOr(config.outputDir), baseName), ~generatedModulePath=`../../generated/${baseName}`) : Pipeline.empty
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
Result.Ok(Pipeline.combine(Array.concat(outputs, [baseWrappers])))
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
| JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
|
|
167
|
+
| _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Generate code from multiple specs (with side effects)
|
|
172
|
+
let generateMultiSpec = async (~baseSpec: openAPISpec, ~forkSpecs: array<forkSpec>, ~config: generationConfig): generationResult =>
|
|
173
|
+
switch generateMultiSpecPure(~baseSpec, ~forkSpecs, ~config) {
|
|
174
|
+
| Result.Error(err) => Result.Error(err)
|
|
175
|
+
| Result.Ok(output) =>
|
|
176
|
+
FileSystem.writeFiles(output.files)
|
|
177
|
+
->Result.map(filePaths => ({generatedFiles: filePaths, diff: None, warnings: output.warnings}: generationSuccess))
|
|
178
|
+
->Result.mapError(errors => UnknownError({message: `Failed to write files: ${Array.join(errors, ", ")}`, context: None}))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Compare two specs and generate diff report
|
|
182
|
+
let compareSpecs = async (~baseSpec, ~forkSpec, ~baseName="base", ~forkName="fork", ~outputPath=?) => {
|
|
183
|
+
let diff = SpecDiffer.generateDiff(~baseSpec, ~forkSpec, ~baseEndpoints=OpenAPIParser.getAllEndpoints(baseSpec), ~forkEndpoints=OpenAPIParser.getAllEndpoints(forkSpec))
|
|
184
|
+
outputPath->Option.forEach(path => {
|
|
185
|
+
let _ = FileSystem.writeFile({path, content: DiffReportGenerator.generateMarkdownReport(~diff, ~baseName, ~forkName)})
|
|
186
|
+
})
|
|
187
|
+
diff
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Main generation function
|
|
191
|
+
let generate = async (config: generationConfig): generationResult => {
|
|
192
|
+
switch await SchemaRefResolver.resolve(config.specPath) {
|
|
193
|
+
| Result.Error(message) => Result.Error(SpecResolutionError({url: config.specPath, message}))
|
|
194
|
+
| Result.Ok(baseSpec) =>
|
|
195
|
+
switch config.forkSpecs {
|
|
196
|
+
| None | Some([]) => await generateSingleSpec(~spec=baseSpec, ~config)
|
|
197
|
+
| Some(forkConfigs) =>
|
|
198
|
+
let forkResults = await forkConfigs
|
|
199
|
+
->Array.map(async f => (await SchemaRefResolver.resolve(f.specPath))->Result.map(spec => ({name: f.name, spec}: forkSpec)))
|
|
200
|
+
->promiseAll
|
|
201
|
+
|
|
202
|
+
switch forkResults->Array.find(Result.isError) {
|
|
203
|
+
| Some(Result.Error(err)) => Result.Error(SpecResolutionError({url: "", message: err}))
|
|
204
|
+
| _ => await generateMultiSpec(~baseSpec, ~forkSpecs=forkResults->Array.filterMap(res => switch res { | Ok(v) => Some(v) | Error(_) => None }), ~config)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let createDefaultConfig = (url, outputDir): generationConfig => ({
|
|
211
|
+
specPath: url, outputDir, strategy: SharedBase, includeTags: None, excludeTags: None,
|
|
212
|
+
modulePerTag: true, generateDiffReport: true, breakingChangeHandling: Warn,
|
|
213
|
+
forkSpecs: None, generateDocOverrides: None, docOverrideDir: None,
|
|
214
|
+
targets: None, dtsOutputDir: None, wrapperOutputDir: None,
|
|
215
|
+
baseInstanceName: None, baseModulePrefix: None,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
let generateFromUrl = async (~url, ~outputDir, ~config=?) =>
|
|
219
|
+
await generate({...config->Option.getOr(createDefaultConfig(url, outputDir)), specPath: url})
|
|
220
|
+
|
|
221
|
+
let generateFromFile = async (~filePath, ~outputDir, ~config=?) =>
|
|
222
|
+
await generate({...config->Option.getOr(createDefaultConfig(filePath, outputDir)), specPath: filePath})
|
package/src/Types.res
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// Types.res - Core OpenAPI and generation types (refactored & compact)
|
|
4
|
+
|
|
5
|
+
// ============= JSON Schema Types =============
|
|
6
|
+
type rec jsonSchemaType =
|
|
7
|
+
| String
|
|
8
|
+
| Number
|
|
9
|
+
| Integer
|
|
10
|
+
| Boolean
|
|
11
|
+
| Array(jsonSchemaType)
|
|
12
|
+
| Object
|
|
13
|
+
| Null
|
|
14
|
+
| Unknown
|
|
15
|
+
|
|
16
|
+
and jsonSchema = {
|
|
17
|
+
@as("type") type_: option<jsonSchemaType>,
|
|
18
|
+
properties: option<dict<jsonSchema>>,
|
|
19
|
+
items: option<jsonSchema>,
|
|
20
|
+
required: option<array<string>>,
|
|
21
|
+
enum: option<array<JSON.t>>,
|
|
22
|
+
@as("$ref") ref: option<string>,
|
|
23
|
+
allOf: option<array<jsonSchema>>,
|
|
24
|
+
oneOf: option<array<jsonSchema>>,
|
|
25
|
+
anyOf: option<array<jsonSchema>>,
|
|
26
|
+
description: option<string>,
|
|
27
|
+
format: option<string>,
|
|
28
|
+
minLength: option<int>,
|
|
29
|
+
maxLength: option<int>,
|
|
30
|
+
minimum: option<float>,
|
|
31
|
+
maximum: option<float>,
|
|
32
|
+
pattern: option<string>,
|
|
33
|
+
nullable: option<bool>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============= OpenAPI 3.1 Types =============
|
|
37
|
+
type httpMethod = [#GET | #POST | #PUT | #PATCH | #DELETE | #HEAD | #OPTIONS]
|
|
38
|
+
|
|
39
|
+
type mediaType = {
|
|
40
|
+
schema: option<jsonSchema>,
|
|
41
|
+
example: option<JSON.t>,
|
|
42
|
+
examples: option<dict<JSON.t>>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type requestBody = {
|
|
46
|
+
description: option<string>,
|
|
47
|
+
content: dict<mediaType>,
|
|
48
|
+
required: option<bool>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type response = {
|
|
52
|
+
description: string,
|
|
53
|
+
content: option<dict<mediaType>>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type parameter = {
|
|
57
|
+
name: string,
|
|
58
|
+
@as("in") in_: string,
|
|
59
|
+
description: option<string>,
|
|
60
|
+
required: option<bool>,
|
|
61
|
+
schema: option<jsonSchema>,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type operation = {
|
|
65
|
+
operationId: option<string>,
|
|
66
|
+
summary: option<string>,
|
|
67
|
+
description: option<string>,
|
|
68
|
+
tags: option<array<string>>,
|
|
69
|
+
requestBody: option<requestBody>,
|
|
70
|
+
responses: dict<response>,
|
|
71
|
+
parameters: option<array<parameter>>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type endpoint = {
|
|
75
|
+
path: string,
|
|
76
|
+
method: string,
|
|
77
|
+
operationId: option<string>,
|
|
78
|
+
summary: option<string>,
|
|
79
|
+
description: option<string>,
|
|
80
|
+
tags: option<array<string>>,
|
|
81
|
+
requestBody: option<requestBody>,
|
|
82
|
+
responses: dict<response>,
|
|
83
|
+
parameters: option<array<parameter>>,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type pathItem = {
|
|
87
|
+
get: option<operation>,
|
|
88
|
+
post: option<operation>,
|
|
89
|
+
put: option<operation>,
|
|
90
|
+
patch: option<operation>,
|
|
91
|
+
delete: option<operation>,
|
|
92
|
+
head: option<operation>,
|
|
93
|
+
options: option<operation>,
|
|
94
|
+
parameters: option<array<parameter>>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type components = {schemas: option<dict<jsonSchema>>}
|
|
98
|
+
|
|
99
|
+
type info = {
|
|
100
|
+
title: string,
|
|
101
|
+
version: string,
|
|
102
|
+
description: option<string>,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type openAPISpec = {
|
|
106
|
+
openapi: string,
|
|
107
|
+
info: info,
|
|
108
|
+
paths: dict<pathItem>,
|
|
109
|
+
components: option<components>,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============= Re-exports from focused modules =============
|
|
113
|
+
// Config types
|
|
114
|
+
type generationStrategy = Config.generationStrategy =
|
|
115
|
+
| Separate
|
|
116
|
+
| SharedBase
|
|
117
|
+
|
|
118
|
+
type breakingChangeHandling = Config.breakingChangeHandling = | Error | Warn | Ignore
|
|
119
|
+
type forkSpecConfig = Config.forkSpecConfig = {name: string, specPath: string}
|
|
120
|
+
type generationTargets = Config.generationTargets = {
|
|
121
|
+
rescriptApi: bool,
|
|
122
|
+
rescriptWrapper: bool,
|
|
123
|
+
typescriptDts: bool,
|
|
124
|
+
typescriptWrapper: bool,
|
|
125
|
+
}
|
|
126
|
+
type generationConfig = Config.t
|
|
127
|
+
|
|
128
|
+
type forkSpec = {
|
|
129
|
+
name: string,
|
|
130
|
+
spec: openAPISpec,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Error types - use `=` syntax to re-export constructors
|
|
134
|
+
type errorContext = CodegenError.context = {
|
|
135
|
+
path: string,
|
|
136
|
+
operation: string,
|
|
137
|
+
schema: option<JSON.t>,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type codegenError = CodegenError.t =
|
|
141
|
+
| SpecResolutionError({url: string, message: string})
|
|
142
|
+
| SchemaParseError({context: errorContext, reason: string})
|
|
143
|
+
| ReferenceError({ref: string, context: errorContext})
|
|
144
|
+
| ValidationError({schema: string, input: JSON.t, issues: array<string>})
|
|
145
|
+
| CircularSchemaError({ref: string, depth: int, path: string})
|
|
146
|
+
| FileWriteError({filePath: string, message: string})
|
|
147
|
+
| InvalidConfigError({field: string, message: string})
|
|
148
|
+
| UnknownError({message: string, context: option<errorContext>})
|
|
149
|
+
|
|
150
|
+
type warning = CodegenError.Warning.t =
|
|
151
|
+
| FallbackToJson({reason: string, context: errorContext})
|
|
152
|
+
| UnsupportedFeature({feature: string, fallback: string, location: string})
|
|
153
|
+
| DepthLimitReached({depth: int, path: string})
|
|
154
|
+
| MissingSchema({ref: string, location: string})
|
|
155
|
+
| IntersectionNotFullySupported({location: string, note: string})
|
|
156
|
+
| ComplexUnionSimplified({location: string, types: string})
|
|
157
|
+
|
|
158
|
+
// ============= Diff Types =============
|
|
159
|
+
type endpointDiff = {
|
|
160
|
+
path: string,
|
|
161
|
+
method: string,
|
|
162
|
+
requestBodyChanged: bool,
|
|
163
|
+
responseChanged: bool,
|
|
164
|
+
breakingChange: bool,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type schemaDiff = {
|
|
168
|
+
name: string,
|
|
169
|
+
breakingChange: bool,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type specDiff = {
|
|
173
|
+
addedEndpoints: array<endpoint>,
|
|
174
|
+
removedEndpoints: array<endpoint>,
|
|
175
|
+
modifiedEndpoints: array<endpointDiff>,
|
|
176
|
+
addedSchemas: array<string>,
|
|
177
|
+
removedSchemas: array<string>,
|
|
178
|
+
modifiedSchemas: array<schemaDiff>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============= Generation Result Types =============
|
|
182
|
+
type generationSuccess = {
|
|
183
|
+
generatedFiles: array<string>,
|
|
184
|
+
diff: option<specDiff>,
|
|
185
|
+
warnings: array<warning>,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type generationResult = result<generationSuccess, codegenError>
|
|
189
|
+
|
|
190
|
+
// ============= Re-export helper modules =============
|
|
191
|
+
module CodegenError = CodegenError
|
|
192
|
+
|
|
193
|
+
module Warning = {
|
|
194
|
+
include CodegenError.Warning
|
|
195
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// CodegenUtils.res - Utility functions for code generation
|
|
4
|
+
|
|
5
|
+
// Convert a string to PascalCase
|
|
6
|
+
@module("@std/text") external toPascalCase: string => string = "toPascalCase"
|
|
7
|
+
|
|
8
|
+
// Convert a string to camelCase
|
|
9
|
+
@module("@std/text") external toCamelCase: string => string = "toCamelCase"
|
|
10
|
+
|
|
11
|
+
// Sanitize identifier (remove special characters, ensure valid ReScript identifier)
|
|
12
|
+
let sanitizeIdentifier = (str: string): string =>
|
|
13
|
+
str
|
|
14
|
+
->String.replaceAll("{", "")->String.replaceAll("}", "")
|
|
15
|
+
->String.replaceAll("[", "")->String.replaceAll("]", "")
|
|
16
|
+
->String.replaceAll(".", "_")->String.replaceAll("-", "_")
|
|
17
|
+
->String.replaceAll("/", "_")->String.replaceAll(" ", "_")
|
|
18
|
+
|
|
19
|
+
// Generate type name from path and method
|
|
20
|
+
let generateTypeName = (~prefix="", path: string, suffix: string): string => {
|
|
21
|
+
let cleaned = path
|
|
22
|
+
->String.replaceAll("/", "_")
|
|
23
|
+
->sanitizeIdentifier
|
|
24
|
+
->String.split("_")
|
|
25
|
+
->Array.filter(part => part != "")
|
|
26
|
+
->Array.map(toPascalCase)
|
|
27
|
+
->Array.join("")
|
|
28
|
+
|
|
29
|
+
prefix ++ cleaned ++ suffix
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Generate operation name from operationId or path + method
|
|
33
|
+
let generateOperationName = (operationId: option<string>, path: string, method: string): string =>
|
|
34
|
+
switch operationId {
|
|
35
|
+
| Some(id) => toCamelCase(sanitizeIdentifier(id))
|
|
36
|
+
| None =>
|
|
37
|
+
method->String.toLowerCase ++ path
|
|
38
|
+
->String.split("/")
|
|
39
|
+
->Array.filter(part => part != "" && !(part->String.startsWith("{")))
|
|
40
|
+
->Array.map(toCamelCase)
|
|
41
|
+
->Array.join("")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Escape ReScript string
|
|
45
|
+
let escapeString = (str: string): string =>
|
|
46
|
+
str
|
|
47
|
+
->String.replaceAll("\\", "\\\\")->String.replaceAll("\"", "\\\"")
|
|
48
|
+
->String.replaceAll("\n", "\\n")->String.replaceAll("\r", "\\r")
|
|
49
|
+
->String.replaceAll("\t", "\\t")
|
|
50
|
+
|
|
51
|
+
// Generate file header
|
|
52
|
+
let generateFileHeader = (~description: string): string =>
|
|
53
|
+
`// ${description}
|
|
54
|
+
// Generated by @f3liz/rescript-autogen-openapi
|
|
55
|
+
// DO NOT EDIT - This file is auto-generated
|
|
56
|
+
|
|
57
|
+
`
|
|
58
|
+
|
|
59
|
+
// Indent code
|
|
60
|
+
let indent = (code: string, level: int): string => {
|
|
61
|
+
let spaces = " "->String.repeat(level)
|
|
62
|
+
code
|
|
63
|
+
->String.split("\n")
|
|
64
|
+
->Array.map(line => line->String.trim == "" ? "" : spaces ++ line)
|
|
65
|
+
->Array.join("\n")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes leading whitespace followed by a margin prefix from every line of a string.
|
|
70
|
+
* This is useful for writing multiline templates in a more readable way.
|
|
71
|
+
*/
|
|
72
|
+
let trimMargin = (text: string, ~marginPrefix="|") => {
|
|
73
|
+
text
|
|
74
|
+
->String.split("\n")
|
|
75
|
+
->Array.map(line => {
|
|
76
|
+
let trimmed = line->String.trimStart
|
|
77
|
+
if trimmed->String.startsWith(marginPrefix) {
|
|
78
|
+
trimmed->String.slice(~start=String.length(marginPrefix))
|
|
79
|
+
} else {
|
|
80
|
+
line
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
->Array.join("\n")
|
|
84
|
+
->String.trim
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ReScript keywords that need to be escaped
|
|
88
|
+
let rescriptKeywords = [
|
|
89
|
+
"and", "as", "assert", "async", "await", "catch", "class", "constraint",
|
|
90
|
+
"do", "done", "downto", "else", "end", "exception", "external", "false",
|
|
91
|
+
"for", "fun", "function", "functor", "if", "in", "include", "inherit",
|
|
92
|
+
"initializer", "lazy", "let", "method", "module", "mutable", "new",
|
|
93
|
+
"nonrec", "object", "of", "open", "or", "private", "rec", "sig", "struct",
|
|
94
|
+
"switch", "then", "to", "true", "try", "type", "val", "virtual", "when",
|
|
95
|
+
"while", "with"
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
// Escape ReScript keywords by adding underscore suffix
|
|
99
|
+
let escapeKeyword = (name: string): string => rescriptKeywords->Array.includes(name) ? name ++ "_" : name
|
|
100
|
+
|
|
101
|
+
// Generate documentation comment (single-line comments)
|
|
102
|
+
let generateDocComment = (~summary=?, ~description=?, ()): string =>
|
|
103
|
+
switch (summary, description) {
|
|
104
|
+
| (None, None) => ""
|
|
105
|
+
| (Some(s), None) => `// ${s}\n`
|
|
106
|
+
| (None, Some(d)) => `// ${d}\n`
|
|
107
|
+
| (Some(s), Some(d)) => `// ${s}\n// ${d}\n`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Generate DocString comment (multi-line /** ... */ format) from markdown
|
|
111
|
+
let generateDocString = (~summary=?, ~description=?, ()): string => {
|
|
112
|
+
let content = switch (summary, description) {
|
|
113
|
+
| (None, None) => None
|
|
114
|
+
| (Some(s), None) => Some(s)
|
|
115
|
+
| (None, Some(d)) => Some(d)
|
|
116
|
+
| (Some(s), Some(d)) => Some(s == d ? s : s ++ "\n\n" ++ d)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
content->Option.map(text => {
|
|
120
|
+
let lines = text->String.trim->String.split("\n")->Array.map(String.trim)
|
|
121
|
+
switch lines {
|
|
122
|
+
| [] => ""
|
|
123
|
+
| [line] => `/** ${line} */\n`
|
|
124
|
+
| lines => "/**\n" ++ lines->Array.map(l => l == "" ? " *" : ` * ${l}`)->Array.join("\n") ++ "\n */\n"
|
|
125
|
+
}
|
|
126
|
+
})->Option.getOr("")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Shared type signature for the fetch function used in generated code
|
|
130
|
+
let fetchTypeSignature = "(~url: string, ~method_: string, ~body: option<JSON.t>) => Promise.t<JSON.t>"
|