@f3liz/rescript-autogen-openapi 0.3.1

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 (72) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +111 -0
  3. package/lib/es6/src/Codegen.d.ts +28 -0
  4. package/lib/es6/src/Codegen.mjs +423 -0
  5. package/lib/es6/src/Types.d.ts +286 -0
  6. package/lib/es6/src/Types.mjs +20 -0
  7. package/lib/es6/src/bindings/Toposort.mjs +12 -0
  8. package/lib/es6/src/core/CodegenUtils.mjs +261 -0
  9. package/lib/es6/src/core/DocOverride.mjs +399 -0
  10. package/lib/es6/src/core/FileSystem.d.ts +4 -0
  11. package/lib/es6/src/core/FileSystem.mjs +78 -0
  12. package/lib/es6/src/core/IRBuilder.mjs +201 -0
  13. package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
  14. package/lib/es6/src/core/Pipeline.d.ts +6 -0
  15. package/lib/es6/src/core/Pipeline.mjs +150 -0
  16. package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
  17. package/lib/es6/src/core/Result.mjs +378 -0
  18. package/lib/es6/src/core/SchemaIR.mjs +425 -0
  19. package/lib/es6/src/core/SchemaIRParser.mjs +683 -0
  20. package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
  21. package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
  22. package/lib/es6/src/core/SpecDiffer.mjs +251 -0
  23. package/lib/es6/src/core/SpecMerger.mjs +237 -0
  24. package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +207 -0
  25. package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
  26. package/lib/es6/src/generators/EndpointGenerator.mjs +173 -0
  27. package/lib/es6/src/generators/IRToSuryGenerator.mjs +543 -0
  28. package/lib/es6/src/generators/IRToTypeGenerator.mjs +592 -0
  29. package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
  30. package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
  31. package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
  32. package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
  33. package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
  34. package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
  35. package/lib/es6/src/types/CodegenError.d.ts +66 -0
  36. package/lib/es6/src/types/CodegenError.mjs +79 -0
  37. package/lib/es6/src/types/Config.d.ts +31 -0
  38. package/lib/es6/src/types/Config.mjs +42 -0
  39. package/lib/es6/src/types/GenerationContext.mjs +47 -0
  40. package/package.json +53 -0
  41. package/rescript.json +26 -0
  42. package/src/Codegen.res +231 -0
  43. package/src/Types.res +222 -0
  44. package/src/bindings/Toposort.res +16 -0
  45. package/src/core/CodegenUtils.res +180 -0
  46. package/src/core/DocOverride.res +504 -0
  47. package/src/core/FileSystem.res +63 -0
  48. package/src/core/IRBuilder.res +66 -0
  49. package/src/core/OpenAPIParser.res +144 -0
  50. package/src/core/Pipeline.res +52 -0
  51. package/src/core/ReferenceResolver.res +41 -0
  52. package/src/core/Result.res +187 -0
  53. package/src/core/SchemaIR.res +291 -0
  54. package/src/core/SchemaIRParser.res +454 -0
  55. package/src/core/SchemaRefResolver.res +143 -0
  56. package/src/core/SchemaRegistry.res +107 -0
  57. package/src/core/SpecDiffer.res +270 -0
  58. package/src/core/SpecMerger.res +245 -0
  59. package/src/generators/ComponentSchemaGenerator.res +210 -0
  60. package/src/generators/DiffReportGenerator.res +152 -0
  61. package/src/generators/EndpointGenerator.res +176 -0
  62. package/src/generators/IRToSuryGenerator.res +386 -0
  63. package/src/generators/IRToTypeGenerator.res +423 -0
  64. package/src/generators/IRToTypeScriptGenerator.res +77 -0
  65. package/src/generators/ModuleGenerator.res +363 -0
  66. package/src/generators/SchemaCodeGenerator.res +84 -0
  67. package/src/generators/ThinWrapperGenerator.res +124 -0
  68. package/src/generators/TypeScriptDtsGenerator.res +193 -0
  69. package/src/generators/TypeScriptWrapperGenerator.res +166 -0
  70. package/src/types/CodegenError.res +85 -0
  71. package/src/types/Config.res +95 -0
  72. package/src/types/GenerationContext.res +56 -0
@@ -0,0 +1,231 @@
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
+ @genType
11
+ let generateSingleSpecPure = (~spec: openAPISpec, ~config: generationConfig): result<Pipeline.t, codegenError> => {
12
+ try {
13
+ let targets = config.targets->Option.getOr(Config.defaultTargets())
14
+ let allEndpoints = OpenAPIParser.getAllEndpoints(spec)
15
+ let endpoints = switch config.includeTags {
16
+ | None => allEndpoints
17
+ | Some(includeTags) => OpenAPIParser.filterByTags(~endpoints=allEndpoints, ~includeTags, ~excludeTags=config.excludeTags->Option.getOr([]))
18
+ }
19
+
20
+ let baseOutput = targets.rescriptApi
21
+ ? Pipeline.combine([
22
+ ComponentSchemaGenerator.generate(~spec, ~outputDir=config.outputDir),
23
+ config.modulePerTag
24
+ ? ModuleGenerator.generateTagModuleFiles(
25
+ ~endpoints,
26
+ ~outputDir=config.outputDir,
27
+ ~overrideDir=?config.docOverrideDir,
28
+ )
29
+ : ModuleGenerator.generateFlatModuleFile(
30
+ ~moduleName="API",
31
+ ~endpoints,
32
+ ~outputDir=config.outputDir,
33
+ ~overrideDir=?config.docOverrideDir,
34
+ ),
35
+ ])
36
+ : Pipeline.empty
37
+
38
+ let wrapperOutput = targets.rescriptWrapper
39
+ ? ThinWrapperGenerator.generateWrapper(~spec, ~endpoints, ~outputDir=config.outputDir, ~wrapperModuleName=CodegenUtils.toPascalCase(spec.info.title) ++ "Wrapper", ~generatedModulePrefix="")
40
+ : Pipeline.empty
41
+
42
+ let dtsOutput = targets.typescriptDts
43
+ ? TypeScriptDtsGenerator.generate(~spec, ~endpoints, ~outputDir=config.dtsOutputDir->Option.getOr(config.outputDir))
44
+ : Pipeline.empty
45
+
46
+ let tsWrapperOutput = targets.typescriptWrapper
47
+ ? TypeScriptWrapperGenerator.generate(~endpoints, ~outputDir=config.wrapperOutputDir->Option.getOr(config.outputDir), ~generatedModulePath="../generated")
48
+ : Pipeline.empty
49
+
50
+ Result.Ok(Pipeline.combine([baseOutput, wrapperOutput, dtsOutput, tsWrapperOutput]))
51
+ } catch {
52
+ | JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
53
+ | _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
54
+ }
55
+ }
56
+
57
+ // Generate code from a single spec (with side effects)
58
+ @genType
59
+ let generateSingleSpec = async (~spec: openAPISpec, ~config: generationConfig): generationResult => {
60
+ switch generateSingleSpecPure(~spec, ~config) {
61
+ | Result.Error(err) => Result.Error(err)
62
+ | Result.Ok(output) =>
63
+ switch FileSystem.writeFiles(output.files) {
64
+ | Result.Error(errors) => Result.Error(UnknownError({message: `Failed to write files: ${Array.join(errors, ", ")}`, context: None}))
65
+ | Result.Ok(filePaths) =>
66
+ let overrideFiles = switch config.generateDocOverrides {
67
+ | Some(true) =>
68
+ let allEndpoints = OpenAPIParser.getAllEndpoints(spec)
69
+ let endpoints = switch config.includeTags {
70
+ | None => allEndpoints
71
+ | Some(includeTags) => OpenAPIParser.filterByTags(~endpoints=allEndpoints, ~includeTags, ~excludeTags=config.excludeTags->Option.getOr([]))
72
+ }
73
+ let files = DocOverride.generateOverrideFiles(~spec, ~endpoints, ~outputDir=config.docOverrideDir->Option.getOr("./docs"), ~host=spec.info.title, ~groupByTag=config.modulePerTag, ())
74
+ FileSystem.writeFiles(files)->Result.getOr([])
75
+ | _ => []
76
+ }
77
+ Result.Ok({generatedFiles: Array.concat(filePaths, overrideFiles), diff: None, warnings: output.warnings})
78
+ }
79
+ }
80
+ }
81
+
82
+ // Process a single fork (pure - returns data)
83
+ let processForkPure = (~baseSpec: openAPISpec, ~baseEndpoints: array<endpoint>, ~fork: forkSpec, ~config: generationConfig): result<Pipeline.t, codegenError> => {
84
+ try {
85
+ let forkEndpoints = OpenAPIParser.getAllEndpoints(fork.spec)
86
+ let diff = SpecDiffer.generateDiff(~baseSpec, ~forkSpec=fork.spec, ~baseEndpoints, ~forkEndpoints)
87
+
88
+ let diffReportFile: option<FileSystem.fileToWrite> = config.generateDiffReport
89
+ ? Some({path: FileSystem.makePath(config.outputDir, `${fork.name}-diff.md`), content: DiffReportGenerator.generateMarkdownReport(~diff, ~baseName="base", ~forkName=fork.name)})
90
+ : None
91
+
92
+ let (sharedSpec, extensionsSpec) = SpecMerger.mergeSpecs(~baseSpec, ~forkSpec=fork.spec, ~baseEndpoints, ~forkEndpoints, ~strategy=config.strategy)
93
+ let sharedEndpoints = OpenAPIParser.getAllEndpoints(sharedSpec)
94
+ let extensionEndpoints = OpenAPIParser.getAllEndpoints(extensionsSpec)
95
+
96
+ let mergeReportFile: FileSystem.fileToWrite = {
97
+ path: FileSystem.makePath(config.outputDir, `${fork.name}-merge.md`),
98
+ 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)
99
+ }
100
+
101
+ let codeOutput = switch config.strategy {
102
+ | Separate =>
103
+ Pipeline.fromFile({
104
+ path: FileSystem.makePath(config.outputDir, `${fork.name}.res`),
105
+ content: ModuleGenerator.generateFlatModuleCode(
106
+ ~moduleName=CodegenUtils.toPascalCase(fork.name),
107
+ ~endpoints=forkEndpoints,
108
+ ~overrideDir=?config.docOverrideDir,
109
+ ),
110
+ })
111
+ | SharedBase =>
112
+ let baseName = config.baseInstanceName->Option.getOrThrow(~message="baseInstanceName required")
113
+ let basePrefix = config.baseModulePrefix->Option.getOr(CodegenUtils.toPascalCase(baseName))
114
+ 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)
115
+ }
116
+
117
+ let targets = config.targets->Option.getOr({rescriptApi: true, rescriptWrapper: false, typescriptDts: false, typescriptWrapper: false})
118
+ let (wSpec, wShared, wExt, wBasePrefix) = switch config.strategy {
119
+ | Separate => (fork.spec, forkEndpoints, [], "")
120
+ | SharedBase => (sharedSpec, sharedEndpoints, extensionEndpoints, config.baseModulePrefix->Option.getOr(config.baseInstanceName->Option.map(CodegenUtils.toPascalCase)->Option.getOr("")))
121
+ }
122
+
123
+ let wrapperOutput = targets.rescriptWrapper
124
+ ? 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)
125
+ : Pipeline.empty
126
+
127
+ let allWEndpoints = Array.concat(wShared, wExt)
128
+ let dtsOutput = targets.typescriptDts
129
+ ? TypeScriptDtsGenerator.generate(~spec=wSpec, ~endpoints=allWEndpoints, ~outputDir=FileSystem.makePath(config.dtsOutputDir->Option.getOr(config.outputDir), fork.name))
130
+ : Pipeline.empty
131
+
132
+ let tsWrapperOutput = targets.typescriptWrapper
133
+ ? TypeScriptWrapperGenerator.generate(~endpoints=allWEndpoints, ~outputDir=FileSystem.makePath(config.wrapperOutputDir->Option.getOr(config.outputDir), fork.name), ~generatedModulePath=`../../generated/${fork.name}`)
134
+ : Pipeline.empty
135
+
136
+ let reports = Pipeline.fromFiles([mergeReportFile, ...diffReportFile->Option.map(f => [f])->Option.getOr([])])
137
+ Result.Ok(Pipeline.combine([reports, codeOutput, wrapperOutput, dtsOutput, tsWrapperOutput]))
138
+ } catch {
139
+ | JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
140
+ | _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
141
+ }
142
+ }
143
+
144
+ // Generate code from multiple specs (pure - returns data)
145
+ @genType
146
+ let generateMultiSpecPure = (~baseSpec: openAPISpec, ~forkSpecs: array<forkSpec>, ~config: generationConfig): result<Pipeline.t, codegenError> => {
147
+ try {
148
+ let baseEndpoints = OpenAPIParser.getAllEndpoints(baseSpec)
149
+ let forkResults = forkSpecs->Array.map(fork => processForkPure(~baseSpec, ~baseEndpoints, ~fork, ~config))
150
+
151
+ switch forkResults->Array.find(Result.isError) {
152
+ | Some(Result.Error(err)) => Result.Error(err)
153
+ | _ =>
154
+ let outputs = forkResults->Array.filterMap(res => switch res { | Ok(v) => Some(v) | Error(_) => None })
155
+ let targets = config.targets->Option.getOr(Config.defaultTargets())
156
+ let baseName = config.baseInstanceName->Option.getOrThrow(~message="baseInstanceName required")
157
+ let basePrefix = config.baseModulePrefix->Option.getOr(CodegenUtils.toPascalCase(baseName))
158
+ let baseOutputDir = FileSystem.makePath(config.outputDir, baseName)
159
+
160
+ let baseWrappers = Pipeline.combine([
161
+ targets.rescriptWrapper ? ThinWrapperGenerator.generateWrapper(~spec=baseSpec, ~endpoints=baseEndpoints, ~outputDir=baseOutputDir, ~wrapperModuleName=basePrefix ++ "Wrapper", ~generatedModulePrefix=basePrefix) : Pipeline.empty,
162
+ targets.typescriptDts ? TypeScriptDtsGenerator.generate(~spec=baseSpec, ~endpoints=baseEndpoints, ~outputDir=FileSystem.makePath(config.dtsOutputDir->Option.getOr(config.outputDir), baseName)) : Pipeline.empty,
163
+ targets.typescriptWrapper ? TypeScriptWrapperGenerator.generate(~endpoints=baseEndpoints, ~outputDir=FileSystem.makePath(config.wrapperOutputDir->Option.getOr(config.outputDir), baseName), ~generatedModulePath=`../../generated/${baseName}`) : Pipeline.empty
164
+ ])
165
+
166
+ Result.Ok(Pipeline.combine(Array.concat(outputs, [baseWrappers])))
167
+ }
168
+ } catch {
169
+ | JsExn(err) => Result.Error(UnknownError({message: err->JsExn.message->Option.getOr("Unknown error"), context: None}))
170
+ | _ => Result.Error(UnknownError({message: "Unknown error", context: None}))
171
+ }
172
+ }
173
+
174
+ // Generate code from multiple specs (with side effects)
175
+ @genType
176
+ let generateMultiSpec = async (~baseSpec: openAPISpec, ~forkSpecs: array<forkSpec>, ~config: generationConfig): generationResult =>
177
+ switch generateMultiSpecPure(~baseSpec, ~forkSpecs, ~config) {
178
+ | Result.Error(err) => Result.Error(err)
179
+ | Result.Ok(output) =>
180
+ FileSystem.writeFiles(output.files)
181
+ ->Result.map(filePaths => ({generatedFiles: filePaths, diff: None, warnings: output.warnings}: generationSuccess))
182
+ ->Result.mapError(errors => UnknownError({message: `Failed to write files: ${Array.join(errors, ", ")}`, context: None}))
183
+ }
184
+
185
+ // Compare two specs and generate diff report
186
+ @genType
187
+ let compareSpecs = async (~baseSpec, ~forkSpec, ~baseName="base", ~forkName="fork", ~outputPath=?) => {
188
+ let diff = SpecDiffer.generateDiff(~baseSpec, ~forkSpec, ~baseEndpoints=OpenAPIParser.getAllEndpoints(baseSpec), ~forkEndpoints=OpenAPIParser.getAllEndpoints(forkSpec))
189
+ outputPath->Option.forEach(path => {
190
+ let _ = FileSystem.writeFile({path, content: DiffReportGenerator.generateMarkdownReport(~diff, ~baseName, ~forkName)})
191
+ })
192
+ diff
193
+ }
194
+
195
+ // Main generation function
196
+ @genType
197
+ let generate = async (config: generationConfig): generationResult => {
198
+ switch await SchemaRefResolver.resolve(config.specPath) {
199
+ | Result.Error(message) => Result.Error(SpecResolutionError({url: config.specPath, message}))
200
+ | Result.Ok(baseSpec) =>
201
+ switch config.forkSpecs {
202
+ | None | Some([]) => await generateSingleSpec(~spec=baseSpec, ~config)
203
+ | Some(forkConfigs) =>
204
+ let forkResults = await forkConfigs
205
+ ->Array.map(async f => (await SchemaRefResolver.resolve(f.specPath))->Result.map(spec => ({name: f.name, spec}: forkSpec)))
206
+ ->promiseAll
207
+
208
+ switch forkResults->Array.find(Result.isError) {
209
+ | Some(Result.Error(err)) => Result.Error(SpecResolutionError({url: "", message: err}))
210
+ | _ => await generateMultiSpec(~baseSpec, ~forkSpecs=forkResults->Array.filterMap(res => switch res { | Ok(v) => Some(v) | Error(_) => None }), ~config)
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ @genType
217
+ let createDefaultConfig = (url, outputDir): generationConfig => ({
218
+ specPath: url, outputDir, strategy: SharedBase, includeTags: None, excludeTags: None,
219
+ modulePerTag: true, generateDiffReport: true, breakingChangeHandling: Warn,
220
+ forkSpecs: None, generateDocOverrides: None, docOverrideDir: None,
221
+ targets: None, dtsOutputDir: None, wrapperOutputDir: None,
222
+ baseInstanceName: None, baseModulePrefix: None,
223
+ })
224
+
225
+ @genType
226
+ let generateFromUrl = async (~url, ~outputDir, ~config=?) =>
227
+ await generate({...config->Option.getOr(createDefaultConfig(url, outputDir)), specPath: url})
228
+
229
+ @genType
230
+ let generateFromFile = async (~filePath, ~outputDir, ~config=?) =>
231
+ await generate({...config->Option.getOr(createDefaultConfig(filePath, outputDir)), specPath: filePath})
package/src/Types.res ADDED
@@ -0,0 +1,222 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // Types.res - Core OpenAPI and generation types (refactored & compact)
4
+
5
+ // ============= JSON Schema Types =============
6
+ @genType
7
+ type rec jsonSchemaType =
8
+ | String
9
+ | Number
10
+ | Integer
11
+ | Boolean
12
+ | Array(jsonSchemaType)
13
+ | Object
14
+ | Null
15
+ | Unknown
16
+
17
+ @genType
18
+ and jsonSchema = {
19
+ @as("type") type_: option<jsonSchemaType>,
20
+ properties: option<dict<jsonSchema>>,
21
+ items: option<jsonSchema>,
22
+ required: option<array<string>>,
23
+ enum: option<array<JSON.t>>,
24
+ @as("$ref") ref: option<string>,
25
+ allOf: option<array<jsonSchema>>,
26
+ oneOf: option<array<jsonSchema>>,
27
+ anyOf: option<array<jsonSchema>>,
28
+ description: option<string>,
29
+ format: option<string>,
30
+ minLength: option<int>,
31
+ maxLength: option<int>,
32
+ minimum: option<float>,
33
+ maximum: option<float>,
34
+ pattern: option<string>,
35
+ nullable: option<bool>,
36
+ }
37
+
38
+ // ============= OpenAPI 3.1 Types =============
39
+ @genType
40
+ type httpMethod = [#GET | #POST | #PUT | #PATCH | #DELETE | #HEAD | #OPTIONS]
41
+
42
+ @genType
43
+ type mediaType = {
44
+ schema: option<jsonSchema>,
45
+ example: option<JSON.t>,
46
+ examples: option<dict<JSON.t>>,
47
+ }
48
+
49
+ @genType
50
+ type requestBody = {
51
+ description: option<string>,
52
+ content: dict<mediaType>,
53
+ required: option<bool>,
54
+ }
55
+
56
+ @genType
57
+ type response = {
58
+ description: string,
59
+ content: option<dict<mediaType>>,
60
+ }
61
+
62
+ @genType
63
+ type parameter = {
64
+ name: string,
65
+ @as("in") in_: string,
66
+ description: option<string>,
67
+ required: option<bool>,
68
+ schema: option<jsonSchema>,
69
+ }
70
+
71
+ @genType
72
+ type operation = {
73
+ operationId: option<string>,
74
+ summary: option<string>,
75
+ description: option<string>,
76
+ tags: option<array<string>>,
77
+ requestBody: option<requestBody>,
78
+ responses: dict<response>,
79
+ parameters: option<array<parameter>>,
80
+ }
81
+
82
+ @genType
83
+ type endpoint = {
84
+ path: string,
85
+ method: string,
86
+ operationId: option<string>,
87
+ summary: option<string>,
88
+ description: option<string>,
89
+ tags: option<array<string>>,
90
+ requestBody: option<requestBody>,
91
+ responses: dict<response>,
92
+ parameters: option<array<parameter>>,
93
+ }
94
+
95
+ @genType
96
+ type pathItem = {
97
+ get: option<operation>,
98
+ post: option<operation>,
99
+ put: option<operation>,
100
+ patch: option<operation>,
101
+ delete: option<operation>,
102
+ head: option<operation>,
103
+ options: option<operation>,
104
+ parameters: option<array<parameter>>,
105
+ }
106
+
107
+ @genType
108
+ type components = {schemas: option<dict<jsonSchema>>}
109
+
110
+ @genType
111
+ type info = {
112
+ title: string,
113
+ version: string,
114
+ description: option<string>,
115
+ }
116
+
117
+ @genType
118
+ type openAPISpec = {
119
+ openapi: string,
120
+ info: info,
121
+ paths: dict<pathItem>,
122
+ components: option<components>,
123
+ }
124
+
125
+ // ============= Re-exports from focused modules =============
126
+ // Config types
127
+ @genType
128
+ type generationStrategy = Config.generationStrategy =
129
+ | Separate
130
+ | SharedBase
131
+
132
+ @genType
133
+ type breakingChangeHandling = Config.breakingChangeHandling = | Error | Warn | Ignore
134
+ @genType
135
+ type forkSpecConfig = Config.forkSpecConfig = {name: string, specPath: string}
136
+ @genType
137
+ type generationTargets = Config.generationTargets = {
138
+ rescriptApi: bool,
139
+ rescriptWrapper: bool,
140
+ typescriptDts: bool,
141
+ typescriptWrapper: bool,
142
+ }
143
+ @genType
144
+ type generationConfig = Config.t
145
+
146
+ @genType
147
+ type forkSpec = {
148
+ name: string,
149
+ spec: openAPISpec,
150
+ }
151
+
152
+ // Error types - use `=` syntax to re-export constructors
153
+ @genType
154
+ type errorContext = CodegenError.context = {
155
+ path: string,
156
+ operation: string,
157
+ schema: option<JSON.t>,
158
+ }
159
+
160
+ @genType
161
+ type codegenError = CodegenError.t =
162
+ | SpecResolutionError({url: string, message: string})
163
+ | SchemaParseError({context: errorContext, reason: string})
164
+ | ReferenceError({ref: string, context: errorContext})
165
+ | ValidationError({schema: string, input: JSON.t, issues: array<string>})
166
+ | CircularSchemaError({ref: string, depth: int, path: string})
167
+ | FileWriteError({filePath: string, message: string})
168
+ | InvalidConfigError({field: string, message: string})
169
+ | UnknownError({message: string, context: option<errorContext>})
170
+
171
+ @genType
172
+ type warning = CodegenError.Warning.t =
173
+ | FallbackToJson({reason: string, context: errorContext})
174
+ | UnsupportedFeature({feature: string, fallback: string, location: string})
175
+ | DepthLimitReached({depth: int, path: string})
176
+ | MissingSchema({ref: string, location: string})
177
+ | IntersectionNotFullySupported({location: string, note: string})
178
+ | ComplexUnionSimplified({location: string, types: string})
179
+
180
+ // ============= Diff Types =============
181
+ @genType
182
+ type endpointDiff = {
183
+ path: string,
184
+ method: string,
185
+ requestBodyChanged: bool,
186
+ responseChanged: bool,
187
+ breakingChange: bool,
188
+ }
189
+
190
+ @genType
191
+ type schemaDiff = {
192
+ name: string,
193
+ breakingChange: bool,
194
+ }
195
+
196
+ @genType
197
+ type specDiff = {
198
+ addedEndpoints: array<endpoint>,
199
+ removedEndpoints: array<endpoint>,
200
+ modifiedEndpoints: array<endpointDiff>,
201
+ addedSchemas: array<string>,
202
+ removedSchemas: array<string>,
203
+ modifiedSchemas: array<schemaDiff>,
204
+ }
205
+
206
+ // ============= Generation Result Types =============
207
+ @genType
208
+ type generationSuccess = {
209
+ generatedFiles: array<string>,
210
+ diff: option<specDiff>,
211
+ warnings: array<warning>,
212
+ }
213
+
214
+ @genType
215
+ type generationResult = result<generationSuccess, codegenError>
216
+
217
+ // ============= Re-export helper modules =============
218
+ module CodegenError = CodegenError
219
+
220
+ module Warning = {
221
+ include CodegenError.Warning
222
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // Toposort.res - ReScript bindings for the toposort npm package
4
+
5
+ type toposortModule
6
+
7
+ @module("toposort") external toposortModule: toposortModule = "default"
8
+
9
+ // toposort.array(nodes, edges) — sort nodes topologically given directed edges
10
+ // edges: array of [from, to] meaning "from depends on to" (to must come before from)
11
+ // Returns sorted array (dependencies first)
12
+ // Throws on cycles
13
+ @send
14
+ external array: (toposortModule, array<string>, array<(string, string)>) => array<string> = "array"
15
+
16
+ let sortArray = (nodes, edges) => toposortModule->array(nodes, edges)
@@ -0,0 +1,180 @@
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("js-convert-case") external toPascalCase: string => string = "toPascalCase"
7
+
8
+ // Convert a string to camelCase
9
+ @module("js-convert-case") 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>"
131
+
132
+ // Generate variant constructor name from an IR type
133
+ let rec variantConstructorName = (irType: SchemaIR.irType): string => {
134
+ switch irType {
135
+ | String(_) => "String"
136
+ | Number(_) => "Float"
137
+ | Integer(_) => "Int"
138
+ | Boolean => "Bool"
139
+ | Null => "Null"
140
+ | Array(_) => "Array"
141
+ | Object(_) => "Object"
142
+ | Reference(ref) =>
143
+ let name = if ref->String.includes("/") {
144
+ ref->String.split("/")->Array.get(ref->String.split("/")->Array.length - 1)->Option.getOr("Ref")
145
+ } else {
146
+ ref
147
+ }
148
+ toPascalCase(name)
149
+ | Literal(StringLiteral(s)) => toPascalCase(s)
150
+ | Literal(NumberLiteral(_)) => "Number"
151
+ | Literal(BooleanLiteral(_)) => "Bool"
152
+ | Literal(NullLiteral) => "Null"
153
+ | Option(inner) => variantConstructorName(inner)
154
+ | Intersection(_) => "Intersection"
155
+ | Union(_) => "Union"
156
+ | Unknown => "Unknown"
157
+ }
158
+ }
159
+
160
+ // Deduplicate variant constructor names by appending counter suffix
161
+ let deduplicateNames = (names: array<string>): array<string> => {
162
+ let counts: Dict.t<int> = Dict.make()
163
+ let result: array<string> = []
164
+ names->Array.forEach(name => {
165
+ let count = counts->Dict.get(name)->Option.getOr(0)
166
+ counts->Dict.set(name, count + 1)
167
+ })
168
+ let seen: Dict.t<int> = Dict.make()
169
+ names->Array.forEach(name => {
170
+ let total = counts->Dict.get(name)->Option.getOr(1)
171
+ if total > 1 {
172
+ let idx = seen->Dict.get(name)->Option.getOr(0) + 1
173
+ seen->Dict.set(name, idx)
174
+ result->Array.push(`${name}${Int.toString(idx)}`)
175
+ } else {
176
+ result->Array.push(name)
177
+ }
178
+ })
179
+ result
180
+ }