@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.
- package/LICENSE +373 -0
- package/README.md +111 -0
- package/lib/es6/src/Codegen.d.ts +28 -0
- package/lib/es6/src/Codegen.mjs +423 -0
- package/lib/es6/src/Types.d.ts +286 -0
- package/lib/es6/src/Types.mjs +20 -0
- package/lib/es6/src/bindings/Toposort.mjs +12 -0
- package/lib/es6/src/core/CodegenUtils.mjs +261 -0
- package/lib/es6/src/core/DocOverride.mjs +399 -0
- package/lib/es6/src/core/FileSystem.d.ts +4 -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.d.ts +6 -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 +425 -0
- package/lib/es6/src/core/SchemaIRParser.mjs +683 -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 +207 -0
- package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
- package/lib/es6/src/generators/EndpointGenerator.mjs +173 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +543 -0
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +592 -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.d.ts +66 -0
- package/lib/es6/src/types/CodegenError.mjs +79 -0
- package/lib/es6/src/types/Config.d.ts +31 -0
- package/lib/es6/src/types/Config.mjs +42 -0
- package/lib/es6/src/types/GenerationContext.mjs +47 -0
- package/package.json +53 -0
- package/rescript.json +26 -0
- package/src/Codegen.res +231 -0
- package/src/Types.res +222 -0
- package/src/bindings/Toposort.res +16 -0
- package/src/core/CodegenUtils.res +180 -0
- package/src/core/DocOverride.res +504 -0
- package/src/core/FileSystem.res +63 -0
- package/src/core/IRBuilder.res +66 -0
- package/src/core/OpenAPIParser.res +144 -0
- package/src/core/Pipeline.res +52 -0
- package/src/core/ReferenceResolver.res +41 -0
- package/src/core/Result.res +187 -0
- package/src/core/SchemaIR.res +291 -0
- package/src/core/SchemaIRParser.res +454 -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 +210 -0
- package/src/generators/DiffReportGenerator.res +152 -0
- package/src/generators/EndpointGenerator.res +176 -0
- package/src/generators/IRToSuryGenerator.res +386 -0
- package/src/generators/IRToTypeGenerator.res +423 -0
- package/src/generators/IRToTypeScriptGenerator.res +77 -0
- package/src/generators/ModuleGenerator.res +363 -0
- package/src/generators/SchemaCodeGenerator.res +84 -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 +85 -0
- package/src/types/Config.res +95 -0
- 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
|
+
}
|