@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,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
+ }