@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
@@ -0,0 +1,199 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // IRToTypeGenerator.res - Generate ReScript types from Schema IR
4
+ open Types
5
+
6
+ let addWarning = GenerationContext.addWarning
7
+
8
+ let rec generateTypeWithContext = (~ctx: GenerationContext.t, ~depth=0, irType: SchemaIR.irType): string => {
9
+ // We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
10
+ if depth > 100 {
11
+ addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
12
+ "JSON.t"
13
+ } else {
14
+ let recurse = nextIrType => generateTypeWithContext(~ctx, ~depth=depth + 1, nextIrType)
15
+
16
+ switch irType {
17
+ | String(_) => "string"
18
+ | Number(_) => "float"
19
+ | Integer(_) => "int"
20
+ | Boolean => "bool"
21
+ | Null => "unit"
22
+ | Array({items}) => `array<${recurse(items)}>`
23
+ | Object({properties, additionalProperties}) =>
24
+ if Array.length(properties) == 0 {
25
+ switch additionalProperties {
26
+ | Some(valueType) => `dict<${recurse(valueType)}>`
27
+ | None => "JSON.t"
28
+ }
29
+ } else {
30
+ let fields =
31
+ properties
32
+ ->Array.map(((name, fieldType, isRequired)) => {
33
+ let typeCode = recurse(fieldType)
34
+ let finalType = isRequired ? typeCode : `option<${typeCode}>`
35
+ let camelName = name->CodegenUtils.toCamelCase
36
+ let escapedName = camelName->CodegenUtils.escapeKeyword
37
+ let aliasAnnotation = escapedName != name ? `@as("${name}") ` : ""
38
+ ` ${aliasAnnotation}${escapedName}: ${finalType},`
39
+ })
40
+ ->Array.join("\n")
41
+ `{\n${fields}\n}`
42
+ }
43
+ | Literal(value) =>
44
+ switch value {
45
+ | StringLiteral(_) => "string"
46
+ | NumberLiteral(_) => "float"
47
+ | BooleanLiteral(_) => "bool"
48
+ | NullLiteral => "unit"
49
+ }
50
+ | Union(types) =>
51
+ // Attempt to simplify common union patterns
52
+ let (hasArray, hasNonArray, arrayItemType, nonArrayType) = types->Array.reduce(
53
+ (false, false, None, None),
54
+ ((hArr, hNonArr, arrItem, nonArr), t) =>
55
+ switch t {
56
+ | Array({items}) => (true, hNonArr, Some(items), nonArr)
57
+ | _ => (hArr, true, arrItem, Some(t))
58
+ },
59
+ )
60
+
61
+ if (
62
+ hasArray &&
63
+ hasNonArray &&
64
+ SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
65
+ ) {
66
+ `array<${recurse(Option.getOr(arrayItemType, Unknown))}>`
67
+ } else if (
68
+ types->Array.every(t =>
69
+ switch t {
70
+ | Literal(StringLiteral(_)) => true
71
+ | _ => false
72
+ }
73
+ ) &&
74
+ Array.length(types) > 0 &&
75
+ Array.length(types) <= 50
76
+ ) {
77
+ let variants =
78
+ types
79
+ ->Array.map(t =>
80
+ switch t {
81
+ | Literal(StringLiteral(s)) => `#${CodegenUtils.toPascalCase(s)}`
82
+ | _ => "#Unknown"
83
+ }
84
+ )
85
+ ->Array.join(" | ")
86
+ `[${variants}]`
87
+ } else {
88
+ addWarning(
89
+ ctx,
90
+ ComplexUnionSimplified({
91
+ location: ctx.path,
92
+ types: types->Array.map(SchemaIR.toString)->Array.join(" | "),
93
+ }),
94
+ )
95
+ "JSON.t"
96
+ }
97
+ | Intersection(types) =>
98
+ // Basic support for intersections by picking the last reference or falling back
99
+ if types->Array.every(t =>
100
+ switch t {
101
+ | Reference(_) => true
102
+ | _ => false
103
+ }
104
+ ) && Array.length(types) > 0 {
105
+ recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
106
+ } else {
107
+ addWarning(
108
+ ctx,
109
+ IntersectionNotFullySupported({location: ctx.path, note: "Complex intersection"}),
110
+ )
111
+ "JSON.t"
112
+ }
113
+ | Option(inner) => `option<${recurse(inner)}>`
114
+ | Reference(ref) =>
115
+ let typePath = switch ctx.availableSchemas {
116
+ | Some(available) =>
117
+ let name =
118
+ ref
119
+ ->String.split("/")
120
+ ->Array.get(ref->String.split("/")->Array.length - 1)
121
+ ->Option.getOr("")
122
+ available->Array.includes(name)
123
+ ? `${CodegenUtils.toPascalCase(name)}.t`
124
+ : `ComponentSchemas.${CodegenUtils.toPascalCase(name)}.t`
125
+ | None =>
126
+ ReferenceResolver.refToTypePath(
127
+ ~insideComponentSchemas=ctx.insideComponentSchemas,
128
+ ~modulePrefix=ctx.modulePrefix,
129
+ ref,
130
+ )->Option.getOr("JSON.t")
131
+ }
132
+ if typePath == "JSON.t" {
133
+ addWarning(
134
+ ctx,
135
+ FallbackToJson({
136
+ reason: `Unresolved ref: ${ref}`,
137
+ context: {path: ctx.path, operation: "gen ref", schema: None},
138
+ }),
139
+ )
140
+ }
141
+ typePath
142
+ | Unknown => "JSON.t"
143
+ }
144
+ }
145
+ }
146
+
147
+ let generateType = (
148
+ ~depth=0,
149
+ ~path="",
150
+ ~insideComponentSchemas=false,
151
+ ~availableSchemas=?,
152
+ ~modulePrefix="",
153
+ irType,
154
+ ) => {
155
+ let ctx = GenerationContext.make(~path, ~insideComponentSchemas, ~availableSchemas?, ~modulePrefix, ())
156
+ (generateTypeWithContext(~ctx, ~depth, irType), ctx.warnings)
157
+ }
158
+
159
+ let generateNamedType = (
160
+ ~namedSchema: SchemaIR.namedSchema,
161
+ ~insideComponentSchemas=false,
162
+ ~availableSchemas=?,
163
+ ~modulePrefix="",
164
+ ) => {
165
+ let ctx = GenerationContext.make(
166
+ ~path=`type.${namedSchema.name}`,
167
+ ~insideComponentSchemas,
168
+ ~availableSchemas?,
169
+ ~modulePrefix,
170
+ (),
171
+ )
172
+ let doc = switch namedSchema.description {
173
+ | Some(d) => CodegenUtils.generateDocString(~description=d, ())
174
+ | None => ""
175
+ }
176
+ (
177
+ `${doc}type ${namedSchema.name} = ${generateTypeWithContext(~ctx, ~depth=0, namedSchema.type_)}`,
178
+ ctx.warnings,
179
+ )
180
+ }
181
+
182
+ let generateAllTypes = (~context: SchemaIR.schemaContext) => {
183
+ let warnings = []
184
+ let types =
185
+ Dict.valuesToArray(context.schemas)
186
+ ->Array.toSorted((a, b) => String.compare(a.name, b.name))
187
+ ->Array.map(s => {
188
+ let (code, w) = generateNamedType(~namedSchema=s)
189
+ warnings->Array.pushMany(w)
190
+ code
191
+ })
192
+ (types, warnings)
193
+ }
194
+
195
+ let generateTypeAndSchema = (~namedSchema) => {
196
+ let (tCode, tW) = generateNamedType(~namedSchema)
197
+ let (sCode, sW) = IRToSuryGenerator.generateNamedSchema(~namedSchema)
198
+ ((tCode, sCode), Array.concat(tW, sW))
199
+ }
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // IRToTypeScriptGenerator.res - Convert SchemaIR to TypeScript types
4
+
5
+ let rec generateType = (~irType: SchemaIR.irType, ~isOptional=false) =>
6
+ switch irType {
7
+ | String(_) => "string"
8
+ | Number(_)
9
+ | Integer(_) => "number"
10
+ | Boolean => "boolean"
11
+ | Null => "null"
12
+ | Unknown => "unknown"
13
+ | Array({items}) => `${generateType(~irType=items)}[]`
14
+ | Object({properties, additionalProperties}) =>
15
+ generateObjectType(~properties, ~additionalProperties)
16
+ | Literal(literal) =>
17
+ switch literal {
18
+ | StringLiteral(s) => `"${s}"`
19
+ | NumberLiteral(n) => Float.toString(n)
20
+ | BooleanLiteral(b) => b ? "true" : "false"
21
+ | NullLiteral => "null"
22
+ }
23
+ | Union(types) => types->Array.map(t => generateType(~irType=t))->Array.join(" | ")
24
+ | Intersection(types) => types->Array.map(t => generateType(~irType=t))->Array.join(" & ")
25
+ | Reference(ref) =>
26
+ switch String.split(ref, "/") {
27
+ | [_, "components", "schemas", name] => `ComponentSchemas.${name}`
28
+ | _ => ref
29
+ }
30
+ | Option(inner) =>
31
+ isOptional ? generateType(~irType=inner, ~isOptional=true) : `${generateType(~irType=inner)} | undefined`
32
+ }
33
+
34
+ and generateObjectType = (~properties, ~additionalProperties) => {
35
+ let propertyLines = properties->Array.map(((name, fieldType, isRequired)) => {
36
+ let (actualType, isFieldOptional) = switch fieldType {
37
+ | SchemaIR.Option(inner) => (inner, true)
38
+ | _ => (fieldType, !isRequired)
39
+ }
40
+ ` ${name}${isFieldOptional ? "?" : ""}: ${generateType(~irType=actualType, ~isOptional=true)};`
41
+ })
42
+
43
+ let additionalPropertiesLines =
44
+ additionalProperties->Option.mapOr([], valueType => [
45
+ ` [key: string]: ${generateType(~irType=valueType)};`,
46
+ ])
47
+
48
+ let allLines = Array.concat(propertyLines, additionalPropertiesLines)
49
+
50
+ if allLines->Array.length == 0 {
51
+ "Record<string, never>"
52
+ } else {
53
+ `{\n${allLines->Array.join("\n")}\n}`
54
+ }
55
+ }
56
+
57
+ let generateNamedType = (~namedSchema: SchemaIR.namedSchema) => {
58
+ let docComment = namedSchema.description->Option.mapOr("", description => `/** ${description} */\n`)
59
+ let typeCode = generateType(~irType=namedSchema.type_)
60
+
61
+ let declaration = switch namedSchema.type_ {
62
+ | Object(_) => `export interface ${namedSchema.name} ${typeCode}`
63
+ | _ => `export type ${namedSchema.name} = ${typeCode};`
64
+ }
65
+
66
+ docComment ++ declaration
67
+ }
68
+
69
+ let generateParameterType = (~name, ~schema: Types.jsonSchema) => {
70
+ let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
71
+ (CodegenUtils.toCamelCase(name), generateType(~irType=ir))
72
+ }
@@ -0,0 +1,362 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // ModuleGenerator.res - Generate API modules organized by tags or in a flat structure
4
+ open Types
5
+
6
+ let generateSchemaCodeForDict = (schemaDict: dict<jsonSchema>) =>
7
+ Dict.toArray(schemaDict)
8
+ ->Array.toSorted(((nameA, _), (nameB, _)) => String.compare(nameA, nameB))
9
+ ->Array.flatMap(((name, schema)) => {
10
+ let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
11
+ let (typeCode, _) = IRToTypeGenerator.generateNamedType(
12
+ ~namedSchema={name: name, description: schema.description, type_: ir},
13
+ )
14
+ let (schemaCode, _) = IRToSuryGenerator.generateNamedSchema(
15
+ ~namedSchema={name: `${name}Schema`, description: schema.description, type_: ir},
16
+ )
17
+ [typeCode->CodegenUtils.indent(2), schemaCode->CodegenUtils.indent(2), ""]
18
+ })
19
+
20
+ let generateTagModulesCode = (endpoints: array<endpoint>, ~overrideDir=?, ~indent=2) => {
21
+ let groupedByTag = OpenAPIParser.groupByTag(endpoints)
22
+ let indentStr = " "->String.repeat(indent)
23
+
24
+ Dict.keysToArray(groupedByTag)
25
+ ->Array.toSorted(String.compare)
26
+ ->Array.filterMap(tag =>
27
+ groupedByTag
28
+ ->Dict.get(tag)
29
+ ->Option.map(tagEndpoints => {
30
+ let moduleName = CodegenUtils.toPascalCase(tag)
31
+ let endpointLines = tagEndpoints->Array.flatMap(endpoint => [
32
+ EndpointGenerator.generateEndpointCode(
33
+ endpoint,
34
+ ~overrideDir?,
35
+ ~moduleName,
36
+ )->CodegenUtils.indent(indent + 2),
37
+ "",
38
+ ])
39
+ Array.concat(
40
+ [`${indentStr}module ${moduleName} = {`],
41
+ Array.concat(endpointLines, [`${indentStr}}`, ""]),
42
+ )
43
+ })
44
+ )
45
+ ->Array.flatMap(lines => lines)
46
+ }
47
+
48
+ let generateTagModuleFile = (
49
+ ~tag,
50
+ ~endpoints,
51
+ ~includeSchemas as _: bool=true,
52
+ ~wrapInModule=false,
53
+ ~overrideDir=?,
54
+ ) => {
55
+ let moduleName = CodegenUtils.toPascalCase(tag)
56
+ let header = CodegenUtils.generateFileHeader(~description=`API endpoints for ${tag}`)
57
+ let body =
58
+ endpoints
59
+ ->Array.map(endpoint =>
60
+ EndpointGenerator.generateEndpointCode(endpoint, ~overrideDir?, ~moduleName)
61
+ )
62
+ ->Array.join("\n\n")
63
+
64
+ if wrapInModule {
65
+ `
66
+ |${header->String.trimEnd}
67
+ |
68
+ |module ${moduleName} = {
69
+ |${body->CodegenUtils.indent(2)}
70
+ |}
71
+ |`->CodegenUtils.trimMargin
72
+ } else {
73
+ `
74
+ |${header->String.trimEnd}
75
+ |
76
+ |${body}
77
+ |`->CodegenUtils.trimMargin
78
+ }
79
+ }
80
+
81
+ let generateAllTagModules = (
82
+ ~endpoints,
83
+ ~includeSchemas=true,
84
+ ~wrapInModule=false,
85
+ ~overrideDir=?,
86
+ ) => {
87
+ let groupedByTag = OpenAPIParser.groupByTag(endpoints)
88
+ Dict.toArray(groupedByTag)
89
+ ->Array.toSorted(((tagA, _), (tagB, _)) => String.compare(tagA, tagB))
90
+ ->Array.map(((tag, tagEndpoints)) => (
91
+ tag,
92
+ generateTagModuleFile(~tag, ~endpoints=tagEndpoints, ~includeSchemas, ~wrapInModule, ~overrideDir?),
93
+ ))
94
+ }
95
+
96
+ let generateIndexModule = (~tags, ~moduleName="API") => {
97
+ let header = CodegenUtils.generateFileHeader(~description="Main API module index")
98
+ let modules =
99
+ tags
100
+ ->Array.toSorted(String.compare)
101
+ ->Array.map(tag => {
102
+ let m = CodegenUtils.toPascalCase(tag)
103
+ ` module ${m} = ${m}`
104
+ })
105
+ ->Array.join("\n")
106
+
107
+ `
108
+ |${header->String.trimEnd}
109
+ |
110
+ |module ${moduleName} = {
111
+ |${modules}
112
+ |}
113
+ |`->CodegenUtils.trimMargin
114
+ }
115
+
116
+ let generateFlatModuleCode = (~moduleName, ~endpoints, ~overrideDir=?) => {
117
+ let header = CodegenUtils.generateFileHeader(~description=`All API endpoints in ${moduleName}`)
118
+ let body =
119
+ endpoints
120
+ ->Array.map(endpoint =>
121
+ EndpointGenerator.generateEndpointCode(
122
+ endpoint,
123
+ ~overrideDir?,
124
+ ~moduleName,
125
+ )->CodegenUtils.indent(2)
126
+ )
127
+ ->Array.join("\n\n")
128
+
129
+ `
130
+ |${header->String.trimEnd}
131
+ |
132
+ |module ${moduleName} = {
133
+ |${body}
134
+ |}
135
+ |`->CodegenUtils.trimMargin
136
+ }
137
+
138
+ let internalGenerateIntegratedModule = (
139
+ ~name,
140
+ ~description,
141
+ ~endpoints,
142
+ ~schemas,
143
+ ~overrideDir=?,
144
+ ~isExtension=false,
145
+ ~includeHeader=true,
146
+ ) => {
147
+ let lines = []
148
+
149
+ if includeHeader {
150
+ lines->Array.pushMany([CodegenUtils.generateFileHeader(~description), ""])
151
+ }
152
+
153
+ lines->Array.push(`module ${name} = {`)
154
+
155
+ schemas->Option.forEach(schemaDict =>
156
+ if Dict.keysToArray(schemaDict)->Array.length > 0 {
157
+ lines->Array.pushMany([
158
+ ` // ${isExtension ? "Extension" : "Component"} Schemas`,
159
+ "",
160
+ ...generateSchemaCodeForDict(schemaDict),
161
+ ])
162
+ }
163
+ )
164
+
165
+ if Array.length(endpoints) > 0 {
166
+ if isExtension {
167
+ lines->Array.pushMany([" // Extension Endpoints", ""])
168
+ }
169
+ lines->Array.pushMany(generateTagModulesCode(endpoints, ~overrideDir?))
170
+ }
171
+
172
+ lines->Array.push("}")
173
+ Array.join(lines, "\n")
174
+ }
175
+
176
+ let generateSharedModule = (~endpoints, ~schemas, ~overrideDir=?, ~includeHeader=true) =>
177
+ internalGenerateIntegratedModule(
178
+ ~name="Shared",
179
+ ~description="Shared API code",
180
+ ~endpoints,
181
+ ~schemas,
182
+ ~overrideDir?,
183
+ ~includeHeader,
184
+ )
185
+
186
+ let generateExtensionModule = (~forkName, ~endpoints, ~schemas, ~overrideDir=?, ~includeHeader=true) =>
187
+ internalGenerateIntegratedModule(
188
+ ~name=`${CodegenUtils.toPascalCase(forkName)}Extensions`,
189
+ ~description=`${forkName} extensions`,
190
+ ~endpoints,
191
+ ~schemas,
192
+ ~overrideDir?,
193
+ ~isExtension=true,
194
+ ~includeHeader,
195
+ )
196
+
197
+ let generateCombinedModule = (
198
+ ~forkName,
199
+ ~sharedEndpoints,
200
+ ~extensionEndpoints,
201
+ ~sharedSchemas,
202
+ ~extensionSchemas,
203
+ ~overrideDir=?,
204
+ ) => {
205
+ let header = CodegenUtils.generateFileHeader(~description=`Combined Shared and ${forkName} extensions`)
206
+
207
+ let shared = generateSharedModule(
208
+ ~endpoints=sharedEndpoints,
209
+ ~schemas=sharedSchemas,
210
+ ~overrideDir?,
211
+ ~includeHeader=false,
212
+ )
213
+
214
+ let extension = generateExtensionModule(
215
+ ~forkName,
216
+ ~endpoints=extensionEndpoints,
217
+ ~schemas=extensionSchemas,
218
+ ~overrideDir?,
219
+ ~includeHeader=false,
220
+ )
221
+
222
+ `
223
+ |${header->String.trimEnd}
224
+ |
225
+ |${shared}
226
+ |
227
+ |${extension}
228
+ |`->CodegenUtils.trimMargin
229
+ }
230
+
231
+ let generateTagModuleFiles = (~endpoints, ~outputDir, ~wrapInModule=false, ~overrideDir=?) => {
232
+ let files =
233
+ generateAllTagModules(~endpoints, ~includeSchemas=true, ~wrapInModule, ~overrideDir?)->Array.map(((
234
+ tag,
235
+ content,
236
+ )) => {
237
+ let path = FileSystem.makePath(outputDir, `${CodegenUtils.toPascalCase(tag)}.res`)
238
+ ({path, content}: FileSystem.fileToWrite)
239
+ })
240
+ Pipeline.fromFilesAndWarnings(files, [])
241
+ }
242
+
243
+ let generateFlatModuleFile = (~moduleName, ~endpoints, ~outputDir, ~overrideDir=?) => {
244
+ let path = FileSystem.makePath(outputDir, `${moduleName}.res`)
245
+ let content = generateFlatModuleCode(~moduleName, ~endpoints, ~overrideDir?)
246
+ Pipeline.fromFile(({path, content}: FileSystem.fileToWrite))
247
+ }
248
+
249
+ let generateInstanceTagModules = (
250
+ ~instanceName,
251
+ ~modulePrefix,
252
+ ~endpoints,
253
+ ~schemas,
254
+ ~outputDir,
255
+ ~overrideDir=?,
256
+ ) => {
257
+ let apiDir = FileSystem.makePath(FileSystem.makePath(outputDir, instanceName), "api")
258
+
259
+ let schemaFiles = schemas->Option.mapOr([], schemaDict =>
260
+ if Dict.keysToArray(schemaDict)->Array.length == 0 {
261
+ []
262
+ } else {
263
+ let result = ComponentSchemaGenerator.generate(
264
+ ~spec={
265
+ openapi: "3.1.0",
266
+ info: {title: instanceName, version: "1.0.0", description: None},
267
+ paths: Dict.make(),
268
+ components: Some({schemas: Some(schemaDict)}),
269
+ },
270
+ ~outputDir=apiDir,
271
+ )
272
+ result.files->Array.map(file =>
273
+ if file.path->String.endsWith("ComponentSchemas.res") {
274
+ {
275
+ ...file,
276
+ path: file.path->String.replace(
277
+ "ComponentSchemas.res",
278
+ `${modulePrefix}ComponentSchemas.res`,
279
+ ),
280
+ }
281
+ } else {
282
+ file
283
+ }
284
+ )
285
+ }
286
+ )
287
+
288
+ let groupedByTag = OpenAPIParser.groupByTag(endpoints)
289
+ let endpointFiles =
290
+ Dict.toArray(groupedByTag)
291
+ ->Array.toSorted(((tagA, _), (tagB, _)) => String.compare(tagA, tagB))
292
+ ->Array.filterMap(((tag, tagEndpoints)) => {
293
+ let moduleName = `${modulePrefix}${CodegenUtils.toPascalCase(tag)}`
294
+ let endpointCodes = tagEndpoints->Array.flatMap(endpoint => [
295
+ EndpointGenerator.generateEndpointCode(endpoint, ~overrideDir?, ~moduleName, ~modulePrefix),
296
+ "",
297
+ ])
298
+ let content = Array.join(
299
+ [
300
+ CodegenUtils.generateFileHeader(~description=`${instanceName} API for ${tag}`),
301
+ "",
302
+ ...endpointCodes,
303
+ ],
304
+ "\n",
305
+ )
306
+ let path = FileSystem.makePath(apiDir, `${moduleName}.res`)
307
+ Some(({path, content}: FileSystem.fileToWrite))
308
+ })
309
+
310
+ Pipeline.fromFilesAndWarnings(Array.concat(schemaFiles, endpointFiles), [])
311
+ }
312
+
313
+ let generateBaseTagModules = (~baseName, ~basePrefix, ~endpoints, ~schemas, ~outputDir, ~overrideDir=?) =>
314
+ generateInstanceTagModules(
315
+ ~instanceName=baseName,
316
+ ~modulePrefix=basePrefix,
317
+ ~endpoints,
318
+ ~schemas,
319
+ ~outputDir,
320
+ ~overrideDir?,
321
+ )
322
+
323
+ let generateForkTagModules = (~forkName, ~forkPrefix, ~endpoints, ~schemas, ~outputDir, ~overrideDir=?) =>
324
+ generateInstanceTagModules(
325
+ ~instanceName=forkName,
326
+ ~modulePrefix=forkPrefix,
327
+ ~endpoints,
328
+ ~schemas,
329
+ ~outputDir,
330
+ ~overrideDir?,
331
+ )
332
+
333
+ let generateSeparatePerTagModules = (
334
+ ~baseName,
335
+ ~basePrefix,
336
+ ~forkName,
337
+ ~forkPrefix=None,
338
+ ~sharedEndpoints,
339
+ ~extensionEndpoints,
340
+ ~sharedSchemas,
341
+ ~extensionSchemas,
342
+ ~outputDir,
343
+ ~overrideDir=?,
344
+ ) =>
345
+ Pipeline.combine([
346
+ generateBaseTagModules(
347
+ ~baseName,
348
+ ~basePrefix,
349
+ ~endpoints=sharedEndpoints,
350
+ ~schemas=sharedSchemas,
351
+ ~outputDir,
352
+ ~overrideDir?,
353
+ ),
354
+ generateForkTagModules(
355
+ ~forkName,
356
+ ~forkPrefix=Option.getOr(forkPrefix, CodegenUtils.toPascalCase(forkName)),
357
+ ~endpoints=extensionEndpoints,
358
+ ~schemas=extensionSchemas,
359
+ ~outputDir,
360
+ ~overrideDir?,
361
+ ),
362
+ ])
@@ -0,0 +1,83 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // SchemaCodeGenerator.res - Generate complete Sury schema code with types
4
+
5
+ let generateTypeCodeAndSchemaCode = (name, schema: Types.jsonSchema) => {
6
+ let (ir, _) = SchemaIRParser.parseJsonSchema(schema)
7
+ let (typeCode, _) = IRToTypeGenerator.generateNamedType(
8
+ ~namedSchema={name: name, description: schema.description, type_: ir},
9
+ )
10
+ let (schemaCode, _) = IRToSuryGenerator.generateNamedSchema(
11
+ ~namedSchema={name: `${name}Schema`, description: schema.description, type_: ir},
12
+ )
13
+ `${typeCode}\n\n${schemaCode}`
14
+ }
15
+
16
+ let generateTypeAndSchema = (name, schema) => generateTypeCodeAndSchemaCode(name, schema)
17
+
18
+ let generateComponentSchemas = (components: option<Types.components>) =>
19
+ components
20
+ ->Option.flatMap(c => c.schemas)
21
+ ->Option.mapOr("// No component schemas\n", schemas => {
22
+ let sections =
23
+ schemas
24
+ ->Dict.toArray
25
+ ->Array.map(((name, schema)) => generateTypeCodeAndSchemaCode(name, schema))
26
+ ->Array.join("\n\n")
27
+ `// Component Schemas\n\n${sections}`
28
+ })
29
+
30
+ let generateOperationSchemas = (operationId, operation: Types.operation) => {
31
+ let generatePart = (suffix, schemaOpt) =>
32
+ schemaOpt->Option.mapOr("", schema =>
33
+ generateTypeCodeAndSchemaCode(`${CodegenUtils.toPascalCase(operationId)}${suffix}`, schema)
34
+ )
35
+
36
+ let requestBodySchema =
37
+ operation.requestBody
38
+ ->Option.flatMap(body => body.content->Dict.get("application/json"))
39
+ ->Option.flatMap(mediaType => mediaType.schema)
40
+
41
+ let successResponseSchema =
42
+ operation.responses
43
+ ->(
44
+ responses =>
45
+ Dict.get(responses, "200")->Option.orElse(Dict.get(responses, "201"))
46
+ )
47
+ ->Option.flatMap(response => response.content)
48
+ ->Option.flatMap(content => content->Dict.get("application/json"))
49
+ ->Option.flatMap(mediaType => mediaType.schema)
50
+
51
+ [
52
+ generatePart("Request", requestBodySchema),
53
+ generatePart("Response", successResponseSchema),
54
+ ]
55
+ ->Array.filter(code => code != "")
56
+ ->Array.join("\n\n")
57
+ }
58
+
59
+ let generateEndpointModule = (path, method, operation: Types.operation) => {
60
+ let operationId = OpenAPIParser.getOperationId(path, method, operation)
61
+ let docComment = CodegenUtils.generateDocComment(
62
+ ~summary=?operation.summary,
63
+ ~description=?operation.description,
64
+ (),
65
+ )
66
+ let methodStr = switch method {
67
+ | #GET => "GET"
68
+ | #POST => "POST"
69
+ | #PUT => "PUT"
70
+ | #PATCH => "PATCH"
71
+ | #DELETE => "DELETE"
72
+ | #HEAD => "HEAD"
73
+ | #OPTIONS => "OPTIONS"
74
+ }
75
+ let schemasCode = generateOperationSchemas(operationId, operation)
76
+
77
+ `${docComment}module ${CodegenUtils.toPascalCase(operationId)} = {
78
+ ${schemasCode->CodegenUtils.indent(2)}
79
+
80
+ let endpoint = "${path}"
81
+ let method = #${methodStr}
82
+ }`
83
+ }