@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,210 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // ComponentSchemaGenerator.res - Generate shared component schema module
4
+ open Types
5
+
6
+ let rec extractReferencedSchemaNames = (irType: SchemaIR.irType) =>
7
+ switch irType {
8
+ | Reference(ref) =>
9
+ // After normalization, ref is just the schema name (no path prefix)
10
+ [ref]
11
+ | Array({items}) => extractReferencedSchemaNames(items)
12
+ | Object({properties}) => properties->Array.flatMap(((_name, fieldType, _)) => extractReferencedSchemaNames(fieldType))
13
+ | Union(types)
14
+ | Intersection(types) =>
15
+ types->Array.flatMap(extractReferencedSchemaNames)
16
+ | Option(inner) => extractReferencedSchemaNames(inner)
17
+ | _ => []
18
+ }
19
+
20
+ let generate = (~spec, ~outputDir) => {
21
+ let (context, parseWarnings) =
22
+ spec.components
23
+ ->Option.flatMap(components => components.schemas)
24
+ ->Option.mapOr(({SchemaIR.schemas: Dict.make()}, []), schemas =>
25
+ SchemaIRParser.parseComponentSchemas(schemas)
26
+ )
27
+
28
+ if Dict.size(context.schemas) == 0 {
29
+ Pipeline.empty
30
+ } else {
31
+ let schemas = Dict.valuesToArray(context.schemas)
32
+ let schemaNameMap = Dict.fromArray(schemas->Array.map(s => (s.name, s)))
33
+
34
+ // Build dependency edges for topological sort
35
+ // Edge (A, B) means "A depends on B" so B must come before A
36
+ let allNodes = schemas->Array.map(s => s.name)
37
+ let edges = schemas->Array.flatMap(schema => {
38
+ let references =
39
+ extractReferencedSchemaNames(schema.type_)->Array.filter(name =>
40
+ Dict.has(schemaNameMap, name) && name != schema.name
41
+ )
42
+ references->Array.map(dep => (schema.name, dep))
43
+ })
44
+
45
+ // Use toposort with cycle tolerance: if there's a cycle, catch and fall back
46
+ // Note: toposort returns dependents first, dependencies last.
47
+ // We reverse to get execution order (dependencies first).
48
+ let sortedNames = try {
49
+ Toposort.sortArray(allNodes, edges)->Array.toReversed
50
+ } catch {
51
+ | _ =>
52
+ // Cycles exist — remove back-edges and re-sort
53
+ let visited = Dict.make()
54
+ let inStack = Dict.make()
55
+ let cycleEdges: array<(string, string)> = []
56
+
57
+ let rec dfs = (node) => {
58
+ if Dict.get(inStack, node)->Option.getOr(false) {
59
+ ()
60
+ } else if Dict.get(visited, node)->Option.getOr(false) {
61
+ ()
62
+ } else {
63
+ Dict.set(visited, node, true)
64
+ Dict.set(inStack, node, true)
65
+ edges->Array.forEach(((from, to)) => {
66
+ if from == node {
67
+ if Dict.get(inStack, to)->Option.getOr(false) {
68
+ cycleEdges->Array.push((from, to))
69
+ } else {
70
+ dfs(to)
71
+ }
72
+ }
73
+ })
74
+ Dict.set(inStack, node, false)
75
+ }
76
+ }
77
+ allNodes->Array.forEach(dfs)
78
+
79
+ let nonCycleEdges = edges->Array.filter(((from, to)) =>
80
+ !(cycleEdges->Array.some(((cf, ct)) => cf == from && ct == to))
81
+ )
82
+ try {
83
+ Toposort.sortArray(allNodes, nonCycleEdges)->Array.toReversed
84
+ } catch {
85
+ | _ => allNodes->Array.toSorted((a, b) => String.compare(a, b))
86
+ }
87
+ }
88
+
89
+ let finalSortedSchemas = sortedNames->Array.filterMap(name => Dict.get(schemaNameMap, name))
90
+ let availableSchemaNames = finalSortedSchemas->Array.map(s => s.name)
91
+ let warnings = Array.copy(parseWarnings)
92
+
93
+ // Detect self-referencing schemas (schema references itself directly or indirectly through properties)
94
+ let selfRefSchemas = Dict.make()
95
+ finalSortedSchemas->Array.forEach(schema => {
96
+ let refs = extractReferencedSchemaNames(schema.type_)
97
+ if refs->Array.some(name => name == schema.name) {
98
+ Dict.set(selfRefSchemas, schema.name, true)
99
+ }
100
+ })
101
+
102
+ let moduleCodes = finalSortedSchemas->Array.map(schema => {
103
+ let isSelfRef = Dict.get(selfRefSchemas, schema.name)->Option.getOr(false)
104
+ let selfRefName = isSelfRef ? Some(schema.name) : None
105
+
106
+ let typeCtx = GenerationContext.make(
107
+ ~path=`ComponentSchemas.${schema.name}`,
108
+ ~insideComponentSchemas=true,
109
+ ~availableSchemas=availableSchemaNames,
110
+ ~selfRefName?,
111
+ (),
112
+ )
113
+
114
+ let typeCode = IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, schema.type_)
115
+
116
+ // Iteratively resolve nested extractions using typeCtx
117
+ let processed = ref(0)
118
+ while processed.contents < Array.length(typeCtx.extractedTypes) {
119
+ let idx = processed.contents
120
+ let {irType, isUnboxed, _}: GenerationContext.extractedType = typeCtx.extractedTypes->Array.getUnsafe(idx)
121
+ if !isUnboxed {
122
+ ignore(IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, ~inline=false, irType))
123
+ } else {
124
+ switch irType {
125
+ | Union(types) =>
126
+ types->Array.forEach(memberType => {
127
+ ignore(IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, ~inline=true, memberType))
128
+ })
129
+ | _ => ignore(IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, ~inline=false, irType))
130
+ }
131
+ }
132
+ processed := idx + 1
133
+ }
134
+
135
+ let allExtracted = Array.copy(typeCtx.extractedTypes)->Array.toReversed
136
+ let extractedTypeMap = if Array.length(allExtracted) > 0 { Some(allExtracted) } else { None }
137
+
138
+ // Generate schema with extracted type map for correct references
139
+ let schemaCtx = GenerationContext.make(
140
+ ~path=`ComponentSchemas.${schema.name}`,
141
+ ~insideComponentSchemas=true,
142
+ ~availableSchemas=availableSchemaNames,
143
+ ~selfRefName?,
144
+ (),
145
+ )
146
+ let schemaCode = IRToSuryGenerator.generateSchemaWithContext(~ctx=schemaCtx, ~depth=0, ~extractedTypeMap?, schema.type_)
147
+
148
+ warnings->Array.pushMany(typeCtx.warnings)
149
+ warnings->Array.pushMany(schemaCtx.warnings)
150
+
151
+ // Generate extracted auxiliary types and schemas (use ctx for dedup)
152
+ let extractedTypeDefs = allExtracted->Array.map(({typeName, irType, isUnboxed}: GenerationContext.extractedType) => {
153
+ let auxTypeCode = if isUnboxed {
154
+ switch irType {
155
+ | Union(types) =>
156
+ let body = IRToTypeGenerator.generateUnboxedVariantBody(~ctx=typeCtx, types)
157
+ `@unboxed type ${typeName} = ${body}`
158
+ | _ =>
159
+ let auxType = IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, irType)
160
+ `type ${typeName} = ${auxType}`
161
+ }
162
+ } else {
163
+ let auxType = IRToTypeGenerator.generateTypeWithContext(~ctx=typeCtx, ~depth=0, irType)
164
+ `type ${typeName} = ${auxType}`
165
+ }
166
+ let auxSchemaCtx = GenerationContext.make(
167
+ ~path=`ComponentSchemas.${schema.name}.${typeName}`,
168
+ ~insideComponentSchemas=true,
169
+ ~availableSchemas=availableSchemaNames,
170
+ (),
171
+ )
172
+ // Exclude the current type from the map to avoid self-reference
173
+ let filteredMap = allExtracted->Array.filter(({typeName: tn}: GenerationContext.extractedType) => tn != typeName)
174
+ let auxExtractedTypeMap = if Array.length(filteredMap) > 0 { Some(filteredMap) } else { None }
175
+ let auxSchema = IRToSuryGenerator.generateSchemaWithContext(~ctx=auxSchemaCtx, ~depth=0, ~extractedTypeMap=?auxExtractedTypeMap, irType)
176
+ ` ${auxTypeCode}\n let ${typeName}Schema = ${auxSchema}`
177
+ })
178
+
179
+ let docComment = schema.description->Option.mapOr("", d =>
180
+ CodegenUtils.generateDocString(~description=d, ())
181
+ )
182
+
183
+ let extractedBlock = if Array.length(extractedTypeDefs) > 0 {
184
+ extractedTypeDefs->Array.join("\n") ++ "\n"
185
+ } else {
186
+ ""
187
+ }
188
+
189
+ // Use `type rec t` for self-referential types
190
+ let typeKeyword = isSelfRef ? "type rec t" : "type t"
191
+ // Wrap schema in S.recursive for self-referential types
192
+ let finalSchemaCode = isSelfRef
193
+ ? `S.recursive("${schema.name}", schema => ${schemaCode})`
194
+ : schemaCode
195
+
196
+ `${docComment}module ${CodegenUtils.toPascalCase(schema.name)} = {
197
+ ${extractedBlock} ${typeKeyword} = ${typeCode}
198
+ let schema = ${finalSchemaCode}
199
+ }`
200
+ })
201
+
202
+ let fileHeader = CodegenUtils.generateFileHeader(~description="Shared component schemas")
203
+ let fileContent = `${fileHeader}\n\n${moduleCodes->Array.join("\n\n")}`
204
+
205
+ Pipeline.fromFilesAndWarnings(
206
+ [{path: FileSystem.makePath(outputDir, "ComponentSchemas.res"), content: fileContent}],
207
+ warnings,
208
+ )
209
+ }
210
+ }
@@ -0,0 +1,152 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // DiffReportGenerator.res - Generate Markdown reports for API diffs and merges
4
+ open Types
5
+
6
+ let formatEndpointName = (endpoint: endpoint) => {
7
+ let methodPart = endpoint.method->String.toUpperCase
8
+ let operationIdPart = endpoint.operationId->Option.mapOr("", id => ` (${id})`)
9
+ `${methodPart} ${endpoint.path}${operationIdPart}`
10
+ }
11
+
12
+ let formatTags = tags =>
13
+ tags->Option.mapOr("", tagList =>
14
+ tagList->Array.length == 0 ? "" : ` [${tagList->Array.join(", ")}]`
15
+ )
16
+
17
+ let generateMarkdownReport = (~diff: specDiff, ~baseName, ~forkName) => {
18
+ let generateSection = (title, items, formatter) =>
19
+ items->Array.length == 0 ? "" : `\n### ${title}\n\n${items->Array.map(formatter)->Array.join("\n")}\n`
20
+
21
+ let totalChanges = SpecDiffer.countChanges(diff)
22
+ let breakingChangesText = SpecDiffer.hasBreakingChanges(diff) ? "⚠️ Yes" : "✓ No"
23
+
24
+ let summaryLines = [
25
+ `- **Total Changes**: ${totalChanges->Int.toString}`,
26
+ `- **Added Endpoints**: ${diff.addedEndpoints->Array.length->Int.toString}`,
27
+ `- **Removed Endpoints**: ${diff.removedEndpoints->Array.length->Int.toString}`,
28
+ `- **Modified Endpoints**: ${diff.modifiedEndpoints->Array.length->Int.toString}`,
29
+ `- **Added Schemas**: ${diff.addedSchemas->Array.length->Int.toString}`,
30
+ `- **Removed Schemas**: ${diff.removedSchemas->Array.length->Int.toString}`,
31
+ `- **Modified Schemas**: ${diff.modifiedSchemas->Array.length->Int.toString}`,
32
+ `- **Breaking Changes**: ${breakingChangesText}`,
33
+ ]->Array.join("\n")
34
+
35
+ let reportParts = [
36
+ `# API Diff Report: ${baseName} → ${forkName}\n\n## Summary\n\n${summaryLines}`,
37
+ generateSection("Added Endpoints", diff.addedEndpoints, (endpoint: endpoint) => {
38
+ let endpointName = formatEndpointName(endpoint)
39
+ let tags = formatTags(endpoint.tags)
40
+ let summary = endpoint.summary->Option.mapOr("", summary => `\n ${summary}`)
41
+ `- **${endpointName}**${tags}${summary}`
42
+ }),
43
+ generateSection("Removed Endpoints", diff.removedEndpoints, (endpoint: endpoint) => {
44
+ let endpointName = formatEndpointName(endpoint)
45
+ let tags = formatTags(endpoint.tags)
46
+ `- **${endpointName}**${tags}`
47
+ }),
48
+ generateSection("Modified Endpoints", diff.modifiedEndpoints, (endpointDiff: endpointDiff) => {
49
+ let methodPart = endpointDiff.method->String.toUpperCase
50
+ let breakingText = endpointDiff.breakingChange ? " **⚠️ BREAKING**" : ""
51
+ let changes =
52
+ [endpointDiff.requestBodyChanged ? "body" : "", endpointDiff.responseChanged ? "response" : ""]
53
+ ->Array.filter(x => x != "")
54
+ ->Array.join(", ")
55
+ `- **${methodPart} ${endpointDiff.path}**${breakingText}: Changed ${changes}`
56
+ }),
57
+ generateSection("Added Schemas", diff.addedSchemas, schemaName => `- \`${schemaName}\``),
58
+ generateSection("Removed Schemas", diff.removedSchemas, schemaName => `- \`${schemaName}\``),
59
+ generateSection("Modified Schemas", diff.modifiedSchemas, (schemaDiff: schemaDiff) => {
60
+ let breakingText = schemaDiff.breakingChange ? " **⚠️ BREAKING**" : ""
61
+ `- \`${schemaDiff.name}\`${breakingText}`
62
+ }),
63
+ `\n---\n*Generated on ${Date.make()->Date.toISOString}*`,
64
+ ]
65
+
66
+ reportParts->Array.filter(part => part != "")->Array.join("\n")
67
+ }
68
+
69
+ let generateCompactSummary = (diff: specDiff) => {
70
+ let totalChanges = SpecDiffer.countChanges(diff)
71
+ let addedCount = diff.addedEndpoints->Array.length
72
+ let removedCount = diff.removedEndpoints->Array.length
73
+ let modifiedCount = diff.modifiedEndpoints->Array.length
74
+ let breakingText = SpecDiffer.hasBreakingChanges(diff) ? " (BREAKING)" : ""
75
+
76
+ `Found ${totalChanges->Int.toString} changes: +${addedCount->Int.toString} -${removedCount->Int.toString} ~${modifiedCount->Int.toString} endpoints${breakingText}`
77
+ }
78
+
79
+ let generateMergeReport = (~stats: SpecMerger.mergeStats, ~baseName, ~forkName) => {
80
+ let sharedEndpoints = stats.sharedEndpointCount->Int.toString
81
+ let sharedSchemas = stats.sharedSchemaCount->Int.toString
82
+ let extensionEndpoints = stats.forkExtensionCount->Int.toString
83
+ let extensionSchemas = stats.forkSchemaCount->Int.toString
84
+
85
+ `
86
+ |# Merge Report: ${baseName} + ${forkName}
87
+ |
88
+ |## Shared Code
89
+ |
90
+ |- **Shared Endpoints**: ${sharedEndpoints}
91
+ |- **Shared Schemas**: ${sharedSchemas}
92
+ |
93
+ |## ${forkName} Extensions
94
+ |
95
+ |- **Extension Endpoints**: ${extensionEndpoints}
96
+ |- **Extension Schemas**: ${extensionSchemas}
97
+ |
98
+ |## Summary
99
+ |
100
+ |The shared base contains ${sharedEndpoints} endpoints and ${sharedSchemas} schemas.
101
+ |
102
+ |${forkName} adds ${extensionEndpoints} endpoints and ${extensionSchemas} schemas.
103
+ |
104
+ |---
105
+ |*Generated on ${Date.make()->Date.toISOString}*
106
+ |`->CodegenUtils.trimMargin
107
+ }
108
+
109
+ let generateEndpointsByTagReport = (endpoints: array<endpoint>) => {
110
+ let endpointsByTag = Dict.make()
111
+ let untaggedEndpoints = []
112
+
113
+ endpoints->Array.forEach(endpoint =>
114
+ switch endpoint.tags {
115
+ | None
116
+ | Some([]) =>
117
+ untaggedEndpoints->Array.push(endpoint)
118
+ | Some(tags) =>
119
+ tags->Array.forEach(tag => {
120
+ let existing = Dict.get(endpointsByTag, tag)->Option.getOr([])
121
+ existing->Array.push(endpoint)
122
+ Dict.set(endpointsByTag, tag, existing)
123
+ })
124
+ }
125
+ )
126
+
127
+ let tagSections =
128
+ Dict.keysToArray(endpointsByTag)
129
+ ->Array.toSorted(String.compare)
130
+ ->Array.map(tag => {
131
+ let tagEndpoints = Dict.get(endpointsByTag, tag)->Option.getOr([])
132
+ let count = tagEndpoints->Array.length->Int.toString
133
+ let endpointList =
134
+ tagEndpoints->Array.map(endpoint => `- ${formatEndpointName(endpoint)}`)->Array.join("\n")
135
+ `### ${tag} (${count})\n\n${endpointList}`
136
+ })
137
+ ->Array.join("\n\n")
138
+
139
+ let untaggedSection =
140
+ untaggedEndpoints->Array.length > 0
141
+ ? {
142
+ let count = untaggedEndpoints->Array.length->Int.toString
143
+ let endpointList =
144
+ untaggedEndpoints
145
+ ->Array.map(endpoint => `- ${formatEndpointName(endpoint)}`)
146
+ ->Array.join("\n")
147
+ `\n\n### Untagged (${count})\n\n${endpointList}`
148
+ }
149
+ : ""
150
+
151
+ `## Endpoints by Tag\n\n${tagSections}${untaggedSection}`
152
+ }
@@ -0,0 +1,176 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // EndpointGenerator.res - Generate API endpoint functions
4
+ open Types
5
+
6
+ let getJsonSchemaFromRequestBody = (requestBody: option<requestBody>) =>
7
+ requestBody->Option.flatMap(body =>
8
+ Dict.toArray(body.content)->Array.get(0)->Option.flatMap(((_contentType, mediaType)) => mediaType.schema)
9
+ )
10
+
11
+ let generateTypeCodeAndSchemaCode = (~jsonSchema, ~typeName, ~schemaName, ~modulePrefix="") => {
12
+ let (ir, _) = SchemaIRParser.parseJsonSchema(jsonSchema)
13
+ let (typeCode, _, extractedTypes) = IRToTypeGenerator.generateNamedType(
14
+ ~namedSchema={name: typeName, description: jsonSchema.description, type_: ir},
15
+ ~modulePrefix,
16
+ )
17
+ let (schemaCode, _) = IRToSuryGenerator.generateNamedSchema(
18
+ ~namedSchema={name: schemaName, description: jsonSchema.description, type_: ir},
19
+ ~modulePrefix,
20
+ ~extractedTypes,
21
+ )
22
+ (typeCode, schemaCode)
23
+ }
24
+
25
+ let generateEndpointFunction = (endpoint: endpoint, ~overrideDir=?, ~moduleName=?) => {
26
+ let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
27
+ let requestTypeName = `${functionName}Request`
28
+ let hasRequestBody = endpoint.requestBody->Option.isSome
29
+ let requestBody = endpoint.requestBody->Option.getOr({
30
+ content: Dict.make(),
31
+ description: None,
32
+ required: Some(false),
33
+ })
34
+ let isRequestBodyRequired = requestBody.required->Option.getOr(false)
35
+
36
+ let bodyParam = hasRequestBody
37
+ ? (isRequestBodyRequired ? `~body: ${requestTypeName}` : `~body: option<${requestTypeName}>=?`)
38
+ : ""
39
+
40
+ // Clean up function signature: handle comma between body and fetch params
41
+ let paramSep = hasRequestBody ? ", " : ""
42
+
43
+ let bodyValueConversion = hasRequestBody
44
+ ? (
45
+ isRequestBodyRequired
46
+ ? ` let jsonBody = body->S.reverseConvertToJsonOrThrow(${functionName}RequestSchema)`
47
+ : ` let jsonBody = body->Option.map(b => b->S.reverseConvertToJsonOrThrow(${functionName}RequestSchema))`
48
+ )
49
+ : ""
50
+
51
+ let successResponse = ["200", "201", "202", "204"]
52
+ ->Array.filterMap(code => Dict.get(endpoint.responses, code))
53
+ ->Array.get(0)
54
+
55
+ let responseHandling = successResponse->Option.mapOr(" response", response =>
56
+ response.content->Option.mapOr(" let _ = response\n ()", content =>
57
+ Dict.toArray(content)->Array.length > 0
58
+ ? ` let value = response->S.parseOrThrow(${functionName}ResponseSchema)\n value`
59
+ : " response"
60
+ )
61
+ )
62
+
63
+ let description = switch (overrideDir, moduleName) {
64
+ | (Some(dir), Some(mName)) =>
65
+ DocOverride.readOverrideWithValidation(
66
+ dir,
67
+ mName,
68
+ functionName,
69
+ DocOverride.generateEndpointHash(endpoint),
70
+ )->(
71
+ overrideResult =>
72
+ switch overrideResult {
73
+ | DocOverride.ValidOverride(v)
74
+ | DocOverride.InvalidHash({override: v}) =>
75
+ Some(v)
76
+ | _ => endpoint.description
77
+ }
78
+ )
79
+ | _ => endpoint.description
80
+ }
81
+
82
+ let docComment = CodegenUtils.generateDocString(
83
+ ~summary=?endpoint.summary,
84
+ ~description=?description,
85
+ (),
86
+ )
87
+
88
+ let code = `
89
+ |${docComment->String.trimEnd}
90
+ |let ${functionName} = (${bodyParam}${paramSep}~fetch: ${CodegenUtils.fetchTypeSignature}): promise<${functionName}Response> => {
91
+ |${bodyValueConversion}
92
+ | fetch(
93
+ | ~url="${endpoint.path}",
94
+ | ~method_="${endpoint.method->String.toUpperCase}",
95
+ | ~body=${hasRequestBody ? "Some(jsonBody)" : "None"},
96
+ | )->Promise.then(response => {
97
+ |${responseHandling}
98
+ | ->Promise.resolve
99
+ | })
100
+ |}`
101
+
102
+ code->CodegenUtils.trimMargin
103
+ }
104
+
105
+ let generateEndpointCode = (endpoint, ~overrideDir=?, ~moduleName=?, ~modulePrefix="") => {
106
+ let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
107
+
108
+ let requestJsonSchema = getJsonSchemaFromRequestBody(endpoint.requestBody)
109
+
110
+ let responseJsonSchema = ["200", "201", "202", "204"]
111
+ ->Array.filterMap(code => Dict.get(endpoint.responses, code))
112
+ ->Array.get(0)
113
+ ->Option.flatMap(resp => resp.content)
114
+ ->Option.flatMap(content =>
115
+ Dict.toArray(content)->Array.get(0)->Option.flatMap(((_contentType, mediaType)) => mediaType.schema)
116
+ )
117
+
118
+ let requestPart = requestJsonSchema->Option.mapOr("", schema => {
119
+ let (typeCode, schemaCode) = generateTypeCodeAndSchemaCode(
120
+ ~jsonSchema=schema,
121
+ ~typeName=`${functionName}Request`,
122
+ ~schemaName=`${functionName}Request`,
123
+ ~modulePrefix,
124
+ )
125
+ `${typeCode}\n\n${schemaCode}`
126
+ })
127
+
128
+ let responsePart = responseJsonSchema->Option.mapOr(`type ${functionName}Response = unit`, schema => {
129
+ let (typeCode, schemaCode) = generateTypeCodeAndSchemaCode(
130
+ ~jsonSchema=schema,
131
+ ~typeName=`${functionName}Response`,
132
+ ~schemaName=`${functionName}Response`,
133
+ ~modulePrefix,
134
+ )
135
+ `${typeCode}\n\n${schemaCode}`
136
+ })
137
+
138
+ [requestPart, responsePart, generateEndpointFunction(endpoint, ~overrideDir?, ~moduleName?)]
139
+ ->Array.filter(s => s != "")
140
+ ->Array.join("\n\n")
141
+ }
142
+
143
+ let generateEndpointModule = (~endpoint, ~modulePrefix="") => {
144
+ let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
145
+ let header = CodegenUtils.generateFileHeader(~description=endpoint.summary->Option.getOr(`API: ${endpoint.path}`))
146
+ `
147
+ |${header->String.trimEnd}
148
+ |
149
+ |module ${CodegenUtils.toPascalCase(functionName)} = {
150
+ |${generateEndpointCode(endpoint, ~modulePrefix)->CodegenUtils.indent(2)}
151
+ |}
152
+ |`->CodegenUtils.trimMargin
153
+ }
154
+
155
+ let generateEndpointsModule = (~moduleName, ~endpoints, ~description=?, ~overrideDir=?, ~modulePrefix="") => {
156
+ let header = CodegenUtils.generateFileHeader(~description=description->Option.getOr(`API for ${moduleName}`))
157
+ let body =
158
+ endpoints
159
+ ->Array.map(ep => generateEndpointCode(ep, ~overrideDir?, ~moduleName, ~modulePrefix)->CodegenUtils.indent(2))
160
+ ->Array.join("\n\n")
161
+
162
+ `
163
+ |${header->String.trimEnd}
164
+ |
165
+ |module ${moduleName} = {
166
+ |${body}
167
+ |}
168
+ |`->CodegenUtils.trimMargin
169
+ }
170
+
171
+ let generateEndpointSignature = (endpoint) => {
172
+ let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
173
+ let summaryPrefix = endpoint.summary->Option.mapOr("", s => `// ${s}\n`)
174
+ let bodyParam = endpoint.requestBody->Option.isSome ? "~body: 'body, " : ""
175
+ `${summaryPrefix}let ${functionName}: (${bodyParam}~fetch: fetchFn) => promise<${functionName}Response>`
176
+ }