@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.
- package/LICENSE +339 -0
- package/README.md +98 -0
- package/lib/es6/src/Codegen.mjs +423 -0
- package/lib/es6/src/Types.mjs +20 -0
- package/lib/es6/src/core/CodegenUtils.mjs +186 -0
- package/lib/es6/src/core/DocOverride.mjs +399 -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.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 +355 -0
- package/lib/es6/src/core/SchemaIRParser.mjs +490 -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 +125 -0
- package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
- package/lib/es6/src/generators/EndpointGenerator.mjs +172 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +233 -0
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +241 -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.mjs +79 -0
- package/lib/es6/src/types/Config.mjs +42 -0
- package/lib/es6/src/types/GenerationContext.mjs +24 -0
- package/package.json +44 -0
- package/rescript.json +20 -0
- package/src/Codegen.res +222 -0
- package/src/Types.res +195 -0
- package/src/core/CodegenUtils.res +130 -0
- package/src/core/DocOverride.res +504 -0
- package/src/core/FileSystem.res +62 -0
- package/src/core/IRBuilder.res +66 -0
- package/src/core/OpenAPIParser.res +144 -0
- package/src/core/Pipeline.res +51 -0
- package/src/core/ReferenceResolver.res +41 -0
- package/src/core/Result.res +187 -0
- package/src/core/SchemaIR.res +258 -0
- package/src/core/SchemaIRParser.res +360 -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 +127 -0
- package/src/generators/DiffReportGenerator.res +152 -0
- package/src/generators/EndpointGenerator.res +172 -0
- package/src/generators/IRToSuryGenerator.res +199 -0
- package/src/generators/IRToTypeGenerator.res +199 -0
- package/src/generators/IRToTypeScriptGenerator.res +72 -0
- package/src/generators/ModuleGenerator.res +362 -0
- package/src/generators/SchemaCodeGenerator.res +83 -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 +82 -0
- package/src/types/Config.res +89 -0
- package/src/types/GenerationContext.res +23 -0
|
@@ -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,172 @@
|
|
|
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, _) = 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
|
+
)
|
|
21
|
+
(typeCode, schemaCode)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let generateEndpointFunction = (endpoint: endpoint, ~overrideDir=?, ~moduleName=?) => {
|
|
25
|
+
let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
|
|
26
|
+
let requestTypeName = `${functionName}Request`
|
|
27
|
+
let hasRequestBody = endpoint.requestBody->Option.isSome
|
|
28
|
+
let requestBody = endpoint.requestBody->Option.getOr({
|
|
29
|
+
content: Dict.make(),
|
|
30
|
+
description: None,
|
|
31
|
+
required: Some(false),
|
|
32
|
+
})
|
|
33
|
+
let isRequestBodyRequired = requestBody.required->Option.getOr(false)
|
|
34
|
+
|
|
35
|
+
let bodyParam = hasRequestBody
|
|
36
|
+
? (isRequestBodyRequired ? `~body: ${requestTypeName}` : `~body: option<${requestTypeName}>=?`)
|
|
37
|
+
: "~body as _"
|
|
38
|
+
|
|
39
|
+
let bodyValueConversion = hasRequestBody
|
|
40
|
+
? (
|
|
41
|
+
isRequestBodyRequired
|
|
42
|
+
? ` let jsonBody = body->S.reverseConvertToJsonOrThrow(${functionName}RequestSchema)`
|
|
43
|
+
: ` let jsonBody = body->Option.map(b => b->S.reverseConvertToJsonOrThrow(${functionName}RequestSchema))`
|
|
44
|
+
)
|
|
45
|
+
: ""
|
|
46
|
+
|
|
47
|
+
let successResponse = ["200", "201", "202", "204"]
|
|
48
|
+
->Array.filterMap(code => Dict.get(endpoint.responses, code))
|
|
49
|
+
->Array.get(0)
|
|
50
|
+
|
|
51
|
+
let responseHandling = successResponse->Option.mapOr(" response", response =>
|
|
52
|
+
response.content->Option.mapOr(" let _ = response\n ()", content =>
|
|
53
|
+
Dict.toArray(content)->Array.length > 0
|
|
54
|
+
? ` let value = response->S.parseOrThrow(${functionName}ResponseSchema)\n value`
|
|
55
|
+
: " response"
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
let description = switch (overrideDir, moduleName) {
|
|
60
|
+
| (Some(dir), Some(mName)) =>
|
|
61
|
+
DocOverride.readOverrideWithValidation(
|
|
62
|
+
dir,
|
|
63
|
+
mName,
|
|
64
|
+
functionName,
|
|
65
|
+
DocOverride.generateEndpointHash(endpoint),
|
|
66
|
+
)->(
|
|
67
|
+
overrideResult =>
|
|
68
|
+
switch overrideResult {
|
|
69
|
+
| DocOverride.ValidOverride(v)
|
|
70
|
+
| DocOverride.InvalidHash({override: v}) =>
|
|
71
|
+
Some(v)
|
|
72
|
+
| _ => endpoint.description
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
| _ => endpoint.description
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let docComment = CodegenUtils.generateDocString(
|
|
79
|
+
~summary=?endpoint.summary,
|
|
80
|
+
~description=?description,
|
|
81
|
+
(),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
let code = `
|
|
85
|
+
|${docComment->String.trimEnd}
|
|
86
|
+
|let ${functionName} = (${bodyParam}, ~fetch: ${CodegenUtils.fetchTypeSignature}): promise<${functionName}Response> => {
|
|
87
|
+
|${bodyValueConversion}
|
|
88
|
+
| fetch(
|
|
89
|
+
| ~url="${endpoint.path}",
|
|
90
|
+
| ~method_="${endpoint.method->String.toUpperCase}",
|
|
91
|
+
| ~body=${hasRequestBody ? "Some(jsonBody)" : "None"},
|
|
92
|
+
| )->Promise.then(response => {
|
|
93
|
+
|${responseHandling}
|
|
94
|
+
| ->Promise.resolve
|
|
95
|
+
| })
|
|
96
|
+
|}`
|
|
97
|
+
|
|
98
|
+
code->CodegenUtils.trimMargin
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let generateEndpointCode = (endpoint, ~overrideDir=?, ~moduleName=?, ~modulePrefix="") => {
|
|
102
|
+
let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
|
|
103
|
+
|
|
104
|
+
let requestJsonSchema = getJsonSchemaFromRequestBody(endpoint.requestBody)
|
|
105
|
+
|
|
106
|
+
let responseJsonSchema = ["200", "201", "202", "204"]
|
|
107
|
+
->Array.filterMap(code => Dict.get(endpoint.responses, code))
|
|
108
|
+
->Array.get(0)
|
|
109
|
+
->Option.flatMap(resp => resp.content)
|
|
110
|
+
->Option.flatMap(content =>
|
|
111
|
+
Dict.toArray(content)->Array.get(0)->Option.flatMap(((_contentType, mediaType)) => mediaType.schema)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
let requestPart = requestJsonSchema->Option.mapOr("", schema => {
|
|
115
|
+
let (typeCode, schemaCode) = generateTypeCodeAndSchemaCode(
|
|
116
|
+
~jsonSchema=schema,
|
|
117
|
+
~typeName=`${functionName}Request`,
|
|
118
|
+
~schemaName=`${functionName}Request`,
|
|
119
|
+
~modulePrefix,
|
|
120
|
+
)
|
|
121
|
+
`${typeCode}\n\n${schemaCode}`
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
let responsePart = responseJsonSchema->Option.mapOr(`type ${functionName}Response = unit`, schema => {
|
|
125
|
+
let (typeCode, schemaCode) = generateTypeCodeAndSchemaCode(
|
|
126
|
+
~jsonSchema=schema,
|
|
127
|
+
~typeName=`${functionName}Response`,
|
|
128
|
+
~schemaName=`${functionName}Response`,
|
|
129
|
+
~modulePrefix,
|
|
130
|
+
)
|
|
131
|
+
`${typeCode}\n\n${schemaCode}`
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
[requestPart, responsePart, generateEndpointFunction(endpoint, ~overrideDir?, ~moduleName?)]
|
|
135
|
+
->Array.filter(s => s != "")
|
|
136
|
+
->Array.join("\n\n")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let generateEndpointModule = (~endpoint, ~modulePrefix="") => {
|
|
140
|
+
let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
|
|
141
|
+
let header = CodegenUtils.generateFileHeader(~description=endpoint.summary->Option.getOr(`API: ${endpoint.path}`))
|
|
142
|
+
`
|
|
143
|
+
|${header->String.trimEnd}
|
|
144
|
+
|
|
|
145
|
+
|module ${CodegenUtils.toPascalCase(functionName)} = {
|
|
146
|
+
|${generateEndpointCode(endpoint, ~modulePrefix)->CodegenUtils.indent(2)}
|
|
147
|
+
|}
|
|
148
|
+
|`->CodegenUtils.trimMargin
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let generateEndpointsModule = (~moduleName, ~endpoints, ~description=?, ~overrideDir=?, ~modulePrefix="") => {
|
|
152
|
+
let header = CodegenUtils.generateFileHeader(~description=description->Option.getOr(`API for ${moduleName}`))
|
|
153
|
+
let body =
|
|
154
|
+
endpoints
|
|
155
|
+
->Array.map(ep => generateEndpointCode(ep, ~overrideDir?, ~moduleName, ~modulePrefix)->CodegenUtils.indent(2))
|
|
156
|
+
->Array.join("\n\n")
|
|
157
|
+
|
|
158
|
+
`
|
|
159
|
+
|${header->String.trimEnd}
|
|
160
|
+
|
|
|
161
|
+
|module ${moduleName} = {
|
|
162
|
+
|${body}
|
|
163
|
+
|}
|
|
164
|
+
|`->CodegenUtils.trimMargin
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let generateEndpointSignature = (endpoint) => {
|
|
168
|
+
let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method)
|
|
169
|
+
let summaryPrefix = endpoint.summary->Option.mapOr("", s => `// ${s}\n`)
|
|
170
|
+
let bodyParam = endpoint.requestBody->Option.isSome ? "~body: 'body, " : ""
|
|
171
|
+
`${summaryPrefix}let ${functionName}: (${bodyParam}~fetch: fetchFn) => promise<${functionName}Response>`
|
|
172
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// IRToSuryGenerator.res - Generate Sury schema code from Schema IR
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
let addWarning = GenerationContext.addWarning
|
|
7
|
+
|
|
8
|
+
let applyConstraints = (base, min, max, toString) => {
|
|
9
|
+
let s1 = switch min {
|
|
10
|
+
| Some(v) => `${base}->S.min(${toString(v)})`
|
|
11
|
+
| None => base
|
|
12
|
+
}
|
|
13
|
+
switch max {
|
|
14
|
+
| Some(v) => `${s1}->S.max(${toString(v)})`
|
|
15
|
+
| None => s1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let rec generateSchemaWithContext = (~ctx: GenerationContext.t, ~depth=0, irType: SchemaIR.irType): string => {
|
|
20
|
+
// We keep a high depth limit just to prevent infinite recursion on circular schemas that escaped IRBuilder
|
|
21
|
+
if depth > 100 {
|
|
22
|
+
addWarning(ctx, DepthLimitReached({depth, path: ctx.path}))
|
|
23
|
+
"S.json"
|
|
24
|
+
} else {
|
|
25
|
+
let recurse = nextIrType => generateSchemaWithContext(~ctx, ~depth=depth + 1, nextIrType)
|
|
26
|
+
|
|
27
|
+
switch irType {
|
|
28
|
+
| String({constraints: c}) =>
|
|
29
|
+
let s = applyConstraints("S.string", c.minLength, c.maxLength, v => Int.toString(v))
|
|
30
|
+
switch c.pattern {
|
|
31
|
+
| Some(p) => `${s}->S.pattern(%re("/${CodegenUtils.escapeString(p)}/"))`
|
|
32
|
+
| None => s
|
|
33
|
+
}
|
|
34
|
+
| Number({constraints: c}) =>
|
|
35
|
+
applyConstraints("S.float", c.minimum, c.maximum, v => Float.toInt(v)->Int.toString)
|
|
36
|
+
| Integer({constraints: c}) =>
|
|
37
|
+
applyConstraints("S.int", c.minimum, c.maximum, v => Float.toInt(v)->Int.toString)
|
|
38
|
+
| Boolean => "S.bool"
|
|
39
|
+
| Null => "S.null"
|
|
40
|
+
| Array({items, constraints: c}) =>
|
|
41
|
+
applyConstraints(`S.array(${recurse(items)})`, c.minItems, c.maxItems, v => Int.toString(v))
|
|
42
|
+
| Object({properties, additionalProperties}) =>
|
|
43
|
+
if Array.length(properties) == 0 {
|
|
44
|
+
switch additionalProperties {
|
|
45
|
+
| Some(valueType) => `S.dict(${recurse(valueType)})`
|
|
46
|
+
| None => "S.json"
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
let fields =
|
|
50
|
+
properties
|
|
51
|
+
->Array.map(((name, fieldType, isRequired)) => {
|
|
52
|
+
let schemaCode = recurse(fieldType)
|
|
53
|
+
let camelName = name->CodegenUtils.toCamelCase->CodegenUtils.escapeKeyword
|
|
54
|
+
isRequired
|
|
55
|
+
? ` ${camelName}: s.field("${name}", ${schemaCode}),`
|
|
56
|
+
: ` ${camelName}: s.fieldOr("${name}", S.nullableAsOption(${schemaCode}), None),`
|
|
57
|
+
})
|
|
58
|
+
->Array.join("\n")
|
|
59
|
+
`S.object(s => {\n${fields}\n })`
|
|
60
|
+
}
|
|
61
|
+
| Literal(value) =>
|
|
62
|
+
switch value {
|
|
63
|
+
| StringLiteral(s) => `S.literal("${CodegenUtils.escapeString(s)}")`
|
|
64
|
+
| NumberLiteral(n) => `S.literal(${Float.toString(n)})`
|
|
65
|
+
| BooleanLiteral(b) => `S.literal(${b ? "true" : "false"})`
|
|
66
|
+
| NullLiteral => "S.literal(null)"
|
|
67
|
+
}
|
|
68
|
+
| Union(types) =>
|
|
69
|
+
let (hasArray, hasNonArray, arrayItemType, nonArrayType) = types->Array.reduce(
|
|
70
|
+
(false, false, None, None),
|
|
71
|
+
((hArr, hNonArr, arrItem, nonArr), t) =>
|
|
72
|
+
switch t {
|
|
73
|
+
| Array({items}) => (true, hNonArr, Some(items), nonArr)
|
|
74
|
+
| _ => (hArr, true, arrItem, Some(t))
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
if (
|
|
78
|
+
hasArray &&
|
|
79
|
+
hasNonArray &&
|
|
80
|
+
SchemaIR.equals(Option.getOr(arrayItemType, Unknown), Option.getOr(nonArrayType, Unknown))
|
|
81
|
+
) {
|
|
82
|
+
`S.array(${recurse(Option.getOr(arrayItemType, Unknown))})`
|
|
83
|
+
} else if (
|
|
84
|
+
types->Array.every(t =>
|
|
85
|
+
switch t {
|
|
86
|
+
| Literal(StringLiteral(_)) => true
|
|
87
|
+
| _ => false
|
|
88
|
+
}
|
|
89
|
+
) &&
|
|
90
|
+
Array.length(types) > 0 &&
|
|
91
|
+
Array.length(types) <= 50
|
|
92
|
+
) {
|
|
93
|
+
`S.union([${types->Array.map(recurse)->Array.join(", ")}])`
|
|
94
|
+
} else {
|
|
95
|
+
addWarning(
|
|
96
|
+
ctx,
|
|
97
|
+
ComplexUnionSimplified({
|
|
98
|
+
location: ctx.path,
|
|
99
|
+
types: types->Array.map(SchemaIR.toString)->Array.join(" | "),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
"S.json"
|
|
103
|
+
}
|
|
104
|
+
| Intersection(types) =>
|
|
105
|
+
if types->Array.every(t =>
|
|
106
|
+
switch t {
|
|
107
|
+
| Reference(_) => true
|
|
108
|
+
| _ => false
|
|
109
|
+
}
|
|
110
|
+
) && Array.length(types) > 0 {
|
|
111
|
+
recurse(types->Array.get(Array.length(types) - 1)->Option.getOr(Unknown))
|
|
112
|
+
} else {
|
|
113
|
+
addWarning(
|
|
114
|
+
ctx,
|
|
115
|
+
IntersectionNotFullySupported({location: ctx.path, note: "Complex intersection"}),
|
|
116
|
+
)
|
|
117
|
+
"S.json"
|
|
118
|
+
}
|
|
119
|
+
| Reference(ref) =>
|
|
120
|
+
let schemaPath = switch ctx.availableSchemas {
|
|
121
|
+
| Some(available) =>
|
|
122
|
+
let name =
|
|
123
|
+
ref
|
|
124
|
+
->String.split("/")
|
|
125
|
+
->Array.get(ref->String.split("/")->Array.length - 1)
|
|
126
|
+
->Option.getOr("")
|
|
127
|
+
available->Array.includes(name)
|
|
128
|
+
? `${CodegenUtils.toPascalCase(name)}.schema`
|
|
129
|
+
: `ComponentSchemas.${CodegenUtils.toPascalCase(name)}.schema`
|
|
130
|
+
| None =>
|
|
131
|
+
ReferenceResolver.refToSchemaPath(
|
|
132
|
+
~insideComponentSchemas=ctx.insideComponentSchemas,
|
|
133
|
+
~modulePrefix=ctx.modulePrefix,
|
|
134
|
+
ref,
|
|
135
|
+
)->Option.getOr("S.json")
|
|
136
|
+
}
|
|
137
|
+
if schemaPath == "S.json" {
|
|
138
|
+
addWarning(
|
|
139
|
+
ctx,
|
|
140
|
+
FallbackToJson({
|
|
141
|
+
reason: `Unresolved ref: ${ref}`,
|
|
142
|
+
context: {path: ctx.path, operation: "gen ref", schema: None},
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
schemaPath
|
|
147
|
+
| Option(inner) => `S.nullableAsOption(${recurse(inner)})`
|
|
148
|
+
| Unknown => "S.json"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let generateSchema = (
|
|
154
|
+
~depth=0,
|
|
155
|
+
~path="",
|
|
156
|
+
~insideComponentSchemas=false,
|
|
157
|
+
~availableSchemas=?,
|
|
158
|
+
~modulePrefix="",
|
|
159
|
+
irType,
|
|
160
|
+
) => {
|
|
161
|
+
let ctx = GenerationContext.make(~path, ~insideComponentSchemas, ~availableSchemas?, ~modulePrefix, ())
|
|
162
|
+
(generateSchemaWithContext(~ctx, ~depth, irType), ctx.warnings)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let generateNamedSchema = (
|
|
166
|
+
~namedSchema: SchemaIR.namedSchema,
|
|
167
|
+
~insideComponentSchemas=false,
|
|
168
|
+
~availableSchemas=?,
|
|
169
|
+
~modulePrefix="",
|
|
170
|
+
) => {
|
|
171
|
+
let ctx = GenerationContext.make(
|
|
172
|
+
~path=`schema.${namedSchema.name}`,
|
|
173
|
+
~insideComponentSchemas,
|
|
174
|
+
~availableSchemas?,
|
|
175
|
+
~modulePrefix,
|
|
176
|
+
(),
|
|
177
|
+
)
|
|
178
|
+
let doc = switch namedSchema.description {
|
|
179
|
+
| Some(d) => CodegenUtils.generateDocComment(~description=d, ())
|
|
180
|
+
| None => ""
|
|
181
|
+
}
|
|
182
|
+
(
|
|
183
|
+
`${doc}let ${namedSchema.name}Schema = ${generateSchemaWithContext(~ctx, ~depth=0, namedSchema.type_)}`,
|
|
184
|
+
ctx.warnings,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let generateAllSchemas = (~context: SchemaIR.schemaContext) => {
|
|
189
|
+
let warnings = []
|
|
190
|
+
let schemas =
|
|
191
|
+
Dict.valuesToArray(context.schemas)
|
|
192
|
+
->Array.toSorted((a, b) => String.compare(a.name, b.name))
|
|
193
|
+
->Array.map(s => {
|
|
194
|
+
let (code, w) = generateNamedSchema(~namedSchema=s)
|
|
195
|
+
warnings->Array.pushMany(w)
|
|
196
|
+
code
|
|
197
|
+
})
|
|
198
|
+
(schemas, warnings)
|
|
199
|
+
}
|