@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
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
+ }
@@ -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>"