@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,360 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // SchemaIRParser.res - Parse JSON Schema to Unified IR
4
+
5
+ // Helper to convert raw JSON type string to our variant
6
+ // This is needed because Obj.magic from JSON gives us raw strings like "string", "object", etc.
7
+ // but our variant constructors compile to "String", "Object", etc. in JS
8
+ let parseTypeString = (rawType: Types.jsonSchemaType): Types.jsonSchemaType => {
9
+ // The rawType might actually be a raw string from JSON, so we need to handle that
10
+ // We use Obj.magic to get the underlying JS value and check it
11
+ let rawStr: string = Obj.magic(rawType)
12
+ switch rawStr {
13
+ | "string" => Types.String
14
+ | "number" => Types.Number
15
+ | "integer" => Types.Integer
16
+ | "boolean" => Types.Boolean
17
+ | "object" => Types.Object
18
+ | "null" => Types.Null
19
+ | "array" => Types.Array(Types.Unknown)
20
+ | _ => {
21
+ // It might already be a proper variant (like when recursively constructed)
22
+ // In that case, rawStr would be "String", "Number", etc.
23
+ switch rawStr {
24
+ | "String" => Types.String
25
+ | "Number" => Types.Number
26
+ | "Integer" => Types.Integer
27
+ | "Boolean" => Types.Boolean
28
+ | "Object" => Types.Object
29
+ | "Null" => Types.Null
30
+ | "Unknown" => Types.Unknown
31
+ | _ => Types.Unknown
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ // Parsing context to collect warnings
38
+ type parsingContext = {
39
+ mutable warnings: array<Types.warning>,
40
+ path: string,
41
+ }
42
+
43
+ let addWarning = (ctx: parsingContext, warning: Types.warning): unit => {
44
+ ctx.warnings->Array.push(warning)
45
+ }
46
+
47
+ // Convert JSON Schema to IR with depth limit to prevent infinite recursion
48
+ let rec parseJsonSchemaWithContext = (
49
+ ~ctx: parsingContext,
50
+ ~depth=0,
51
+ schema: Types.jsonSchema,
52
+ ): SchemaIR.irType => {
53
+ // Safety: Prevent infinite recursion on circular schemas
54
+ if depth > 30 {
55
+ addWarning(
56
+ ctx,
57
+ DepthLimitReached({
58
+ depth: depth,
59
+ path: ctx.path,
60
+ }),
61
+ )
62
+ SchemaIR.Unknown
63
+ } else {
64
+ // Handle $ref first
65
+ switch schema.ref {
66
+ | Some(ref) => SchemaIR.Reference(ref)
67
+ | None => {
68
+ // Check if nullable
69
+ let isNullable = schema.nullable->Option.getOr(false)
70
+
71
+ // Normalize the type field (raw JSON strings like "string" -> variant String)
72
+ let normalizedType = schema.type_->Option.map(parseTypeString)
73
+
74
+ // Parse base type
75
+ let baseType = switch normalizedType {
76
+ | Some(Types.String) => {
77
+ let constraints: SchemaIR.stringConstraints = {
78
+ minLength: schema.minLength,
79
+ maxLength: schema.maxLength,
80
+ pattern: schema.pattern,
81
+ }
82
+ SchemaIR.String({constraints: constraints})
83
+ }
84
+ | Some(Types.Number) => {
85
+ let constraints: SchemaIR.numberConstraints = {
86
+ minimum: schema.minimum,
87
+ maximum: schema.maximum,
88
+ multipleOf: None, // Not in jsonSchema type
89
+ }
90
+ SchemaIR.Number({constraints: constraints})
91
+ }
92
+ | Some(Types.Integer) => {
93
+ let constraints: SchemaIR.numberConstraints = {
94
+ minimum: schema.minimum,
95
+ maximum: schema.maximum,
96
+ multipleOf: None, // Not in jsonSchema type
97
+ }
98
+ SchemaIR.Integer({constraints: constraints})
99
+ }
100
+ | Some(Types.Boolean) => SchemaIR.Boolean
101
+ | Some(Types.Null) => SchemaIR.Null
102
+ | Some(Types.Array(_)) => {
103
+ let items = switch schema.items {
104
+ | None => SchemaIR.Unknown
105
+ | Some(itemSchema) =>
106
+ parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, itemSchema)
107
+ }
108
+
109
+ let constraints: SchemaIR.arrayConstraints = {
110
+ minItems: None, // Not in current jsonSchema type
111
+ maxItems: None, // Not in current jsonSchema type
112
+ uniqueItems: false,
113
+ }
114
+
115
+ SchemaIR.Array({items, constraints})
116
+ }
117
+ | Some(Types.Object) => {
118
+ // Check if this is an allOf composition (common in OpenAPI)
119
+ switch schema.allOf {
120
+ | Some(schemas) => {
121
+ // allOf with type: "object" - parse as intersection
122
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
123
+ SchemaIR.Intersection(types)
124
+ }
125
+ | None => {
126
+ // Regular object type - parse properties
127
+ let properties = switch schema.properties {
128
+ | None => []
129
+ | Some(propsDict) => {
130
+ let required = schema.required->Option.getOr([])
131
+ Dict.toArray(propsDict)->Array.map(((name, propSchema)) => {
132
+ let isRequired = required->Array.includes(name)
133
+ let propType = parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, propSchema)
134
+ (name, propType, isRequired)
135
+ })
136
+ }
137
+ }
138
+
139
+ // additionalProperties not in current jsonSchema type
140
+ let additionalProperties = None
141
+
142
+ SchemaIR.Object({
143
+ properties,
144
+ additionalProperties,
145
+ })
146
+ }
147
+ }
148
+ }
149
+ | Some(Types.Unknown) => SchemaIR.Unknown
150
+ | None => {
151
+ // No type specified, check for enum, properties, or combinators
152
+ switch (schema.enum, schema.properties, schema.allOf, schema.oneOf, schema.anyOf) {
153
+ | (Some(enumValues), _, _, _, _) => {
154
+ // Enum - convert to union of literals
155
+ let literals = enumValues->Array.map(value => {
156
+ switch value {
157
+ | String(str) => SchemaIR.Literal(SchemaIR.StringLiteral(str))
158
+ | Number(num) => SchemaIR.Literal(SchemaIR.NumberLiteral(num))
159
+ | Boolean(b) => SchemaIR.Literal(SchemaIR.BooleanLiteral(b))
160
+ | Null => SchemaIR.Literal(SchemaIR.NullLiteral)
161
+ | _ => SchemaIR.Unknown
162
+ }
163
+ })
164
+ SchemaIR.Union(literals)
165
+ }
166
+ | (_, Some(_), _, _, _) => {
167
+ // Has properties, treat as object
168
+ parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, {...schema, type_: Some(Object)})
169
+ }
170
+ | (_, _, Some(schemas), _, _) => {
171
+ // allOf - intersection
172
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
173
+ SchemaIR.Intersection(types)
174
+ }
175
+ | (_, _, _, Some(schemas), _) => {
176
+ // oneOf - union
177
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
178
+ SchemaIR.Union(types)
179
+ }
180
+ | (_, _, _, _, Some(schemas)) => {
181
+ // anyOf - union
182
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
183
+ SchemaIR.Union(types)
184
+ }
185
+ | _ => SchemaIR.Unknown
186
+ }
187
+ }
188
+ }
189
+
190
+ // Wrap in Option if nullable
191
+ if isNullable {
192
+ SchemaIR.Option(baseType)
193
+ } else {
194
+ baseType
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ // Convenience wrapper that creates a context and returns warnings
202
+ let parseJsonSchema = (~depth=0, schema: Types.jsonSchema): (SchemaIR.irType, array<Types.warning>) => {
203
+ let ctx = {warnings: [], path: "root"}
204
+ let irType = parseJsonSchemaWithContext(~ctx, ~depth, schema)
205
+ (irType, ctx.warnings)
206
+ }
207
+
208
+ // Parse a named schema
209
+ let parseNamedSchema = (~name: string, ~schema: Types.jsonSchema): (SchemaIR.namedSchema, array<Types.warning>) => {
210
+ let ctx = {warnings: [], path: `components.schemas.${name}`}
211
+ let type_ = parseJsonSchemaWithContext(~ctx, schema)
212
+ ({
213
+ name,
214
+ description: schema.description,
215
+ type_,
216
+ }, ctx.warnings)
217
+ }
218
+
219
+ // Parse all component schemas
220
+ let parseComponentSchemas = (schemas: dict<Types.jsonSchema>): (SchemaIR.schemaContext, array<Types.warning>) => {
221
+ let namedSchemas = Dict.make()
222
+ let allWarnings = []
223
+
224
+ schemas->Dict.toArray->Array.forEach(((name, schema)) => {
225
+ let (namedSchema, warnings) = parseNamedSchema(~name, ~schema)
226
+ Dict.set(namedSchemas, name, namedSchema)
227
+ allWarnings->Array.pushMany(warnings)
228
+ })
229
+
230
+ ({schemas: namedSchemas}, allWarnings)
231
+ }
232
+
233
+ // Resolve a reference in the context
234
+ let resolveReference = (
235
+ ~context: SchemaIR.schemaContext,
236
+ ~ref: string,
237
+ ): option<SchemaIR.namedSchema> => {
238
+ // Handle #/components/schemas/SchemaName format
239
+ let parts = ref->String.split("/")
240
+ switch parts->Array.get(parts->Array.length - 1) {
241
+ | None => None
242
+ | Some(schemaName) => Dict.get(context.schemas, schemaName)
243
+ }
244
+ }
245
+
246
+ // Inline simple references to reduce indirection
247
+ let rec inlineSimpleReferences = (
248
+ ~context: SchemaIR.schemaContext,
249
+ ~irType: SchemaIR.irType,
250
+ ~depth: int=0,
251
+ ~maxDepth: int=2,
252
+ ): SchemaIR.irType => {
253
+ if depth >= maxDepth {
254
+ irType
255
+ } else {
256
+ switch irType {
257
+ | SchemaIR.Reference(ref) => {
258
+ switch resolveReference(~context, ~ref) {
259
+ | None => irType
260
+ | Some(schema) => {
261
+ // Only inline if it's a simple type
262
+ if SchemaIR.isSimpleType(schema.type_) {
263
+ inlineSimpleReferences(~context, ~irType=schema.type_, ~depth=depth + 1, ~maxDepth)
264
+ } else {
265
+ irType
266
+ }
267
+ }
268
+ }
269
+ }
270
+ | SchemaIR.Option(inner) =>
271
+ SchemaIR.Option(inlineSimpleReferences(~context, ~irType=inner, ~depth, ~maxDepth))
272
+ | SchemaIR.Array({items, constraints}) =>
273
+ SchemaIR.Array({
274
+ items: inlineSimpleReferences(~context, ~irType=items, ~depth, ~maxDepth),
275
+ constraints,
276
+ })
277
+ | SchemaIR.Object({properties, additionalProperties}) => {
278
+ let newProperties = properties->Array.map(((name, type_, required)) => {
279
+ (name, inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth), required)
280
+ })
281
+ let newAdditionalProps = additionalProperties->Option.map(type_ =>
282
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
283
+ )
284
+ SchemaIR.Object({
285
+ properties: newProperties,
286
+ additionalProperties: newAdditionalProps,
287
+ })
288
+ }
289
+ | SchemaIR.Union(types) =>
290
+ SchemaIR.Union(types->Array.map(type_ =>
291
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
292
+ ))
293
+ | SchemaIR.Intersection(types) =>
294
+ SchemaIR.Intersection(types->Array.map(type_ =>
295
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
296
+ ))
297
+ | other => other
298
+ }
299
+ }
300
+ }
301
+
302
+ // Optimize IR by simplifying unions, intersections, etc.
303
+ let rec optimizeIR = (irType: SchemaIR.irType): SchemaIR.irType => {
304
+ switch irType {
305
+ | SchemaIR.Union(types) => {
306
+ // Flatten nested unions
307
+ let flattened = types->Array.flatMap(t => {
308
+ switch optimizeIR(t) {
309
+ | SchemaIR.Union(inner) => inner
310
+ | other => [other]
311
+ }
312
+ })
313
+
314
+ // Remove duplicates (simple dedup by toString)
315
+ let unique = []
316
+ flattened->Array.forEach(type_ => {
317
+ let typeStr = SchemaIR.toString(type_)
318
+ let exists = unique->Array.some(t => SchemaIR.toString(t) == typeStr)
319
+ if !exists { unique->Array.push(type_) }
320
+ })
321
+
322
+ // Simplify single-element unions
323
+ switch unique {
324
+ | [] => SchemaIR.Unknown
325
+ | [single] => single
326
+ | multiple => SchemaIR.Union(multiple)
327
+ }
328
+ }
329
+ | SchemaIR.Intersection(types) => {
330
+ // Flatten nested intersections
331
+ let flattened = types->Array.flatMap(t => {
332
+ switch optimizeIR(t) {
333
+ | SchemaIR.Intersection(inner) => inner
334
+ | other => [other]
335
+ }
336
+ })
337
+
338
+ // Simplify single-element intersections
339
+ switch flattened {
340
+ | [] => SchemaIR.Unknown
341
+ | [single] => single
342
+ | multiple => SchemaIR.Intersection(multiple)
343
+ }
344
+ }
345
+ | SchemaIR.Option(inner) => SchemaIR.Option(optimizeIR(inner))
346
+ | SchemaIR.Array({items, constraints}) =>
347
+ SchemaIR.Array({items: optimizeIR(items), constraints})
348
+ | SchemaIR.Object({properties, additionalProperties}) => {
349
+ let newProperties = properties->Array.map(((name, type_, required)) => {
350
+ (name, optimizeIR(type_), required)
351
+ })
352
+ let newAdditionalProps = additionalProperties->Option.map(optimizeIR)
353
+ SchemaIR.Object({
354
+ properties: newProperties,
355
+ additionalProperties: newAdditionalProps,
356
+ })
357
+ }
358
+ | other => other
359
+ }
360
+ }
@@ -0,0 +1,143 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // SchemaRefResolver.res - Bindings to @readme/openapi-parser
4
+
5
+ // Parser options type
6
+ type parserOptions = {
7
+ timeoutMs?: int,
8
+ }
9
+
10
+ // External bindings to @readme/openapi-parser v5.x
11
+ @module("@readme/openapi-parser")
12
+ external parse: (string, ~options: parserOptions=?) => promise<JSON.t> = "parse"
13
+
14
+ @module("@readme/openapi-parser")
15
+ external bundle: (string, ~options: parserOptions=?) => promise<JSON.t> = "bundle"
16
+
17
+ @module("@readme/openapi-parser")
18
+ external dereference: (string, ~options: parserOptions=?) => promise<JSON.t> = "dereference"
19
+
20
+ @module("@readme/openapi-parser")
21
+ external validate: (string, ~options: parserOptions=?) => promise<JSON.t> = "validate"
22
+
23
+ // Helper to convert JSON to OpenAPISpec
24
+ let jsonToSpec = (json: JSON.t): result<Types.openAPISpec, string> => {
25
+ switch json->JSON.Decode.object {
26
+ | Some(obj) => {
27
+ // Convert to our OpenAPISpec type
28
+ let pathsDict = obj
29
+ ->Dict.get("paths")
30
+ ->Option.flatMap(JSON.Decode.object)
31
+ ->Option.getOr(Dict.make())
32
+
33
+ let openAPISpec: Types.openAPISpec = {
34
+ openapi: obj
35
+ ->Dict.get("openapi")
36
+ ->Option.flatMap(JSON.Decode.string)
37
+ ->Option.getOr("3.1.0"),
38
+ info: {
39
+ let infoObj = obj
40
+ ->Dict.get("info")
41
+ ->Option.flatMap(JSON.Decode.object)
42
+ ->Option.getOr(Dict.make())
43
+ {
44
+ title: infoObj
45
+ ->Dict.get("title")
46
+ ->Option.flatMap(JSON.Decode.string)
47
+ ->Option.getOr("Untitled API"),
48
+ version: infoObj
49
+ ->Dict.get("version")
50
+ ->Option.flatMap(JSON.Decode.string)
51
+ ->Option.getOr("0.0.0"),
52
+ description: infoObj
53
+ ->Dict.get("description")
54
+ ->Option.flatMap(JSON.Decode.string),
55
+ }
56
+ },
57
+ paths: pathsDict
58
+ ->Dict.toArray
59
+ ->Array.map(((key, _value)) => (key, Obj.magic(_value)))
60
+ ->Dict.fromArray,
61
+ components: obj
62
+ ->Dict.get("components")
63
+ ->Option.map(_comp => ({
64
+ schemas: _comp
65
+ ->JSON.Decode.object
66
+ ->Option.flatMap(c => c->Dict.get("schemas"))
67
+ ->Option.flatMap(JSON.Decode.object)
68
+ ->Option.map(schemas =>
69
+ schemas
70
+ ->Dict.toArray
71
+ ->Array.map(((key, value)) => (key, Obj.magic(value)))
72
+ ->Dict.fromArray
73
+ ),
74
+ }: Types.components)),
75
+ }
76
+
77
+ Ok(openAPISpec)
78
+ }
79
+ | None => Error("Invalid OpenAPI spec: root is not an object")
80
+ }
81
+ }
82
+
83
+ // Resolve a spec from source
84
+ let resolve = async (source: string, ~timeout: option<int>=?): result<Types.openAPISpec, string> => {
85
+ try {
86
+ // Use bundle to combine all external refs while keeping internal $refs intact
87
+ let options = {timeoutMs: timeout->Option.getOr(60000)}
88
+ let resolved = await bundle(source, ~options)
89
+
90
+ jsonToSpec(resolved)
91
+ } catch {
92
+ | JsExn(err) => {
93
+ let message = err->JsExn.message->Option.getOr("Unknown error resolving spec")
94
+ Error(`Failed to resolve spec: ${message}`)
95
+ }
96
+ | _ => Error("Unknown error resolving spec")
97
+ }
98
+ }
99
+
100
+ // Parse an OpenAPI spec without dereferencing
101
+ let parseOnly = async (source: string, ~timeout: option<int>=?): result<Types.openAPISpec, string> => {
102
+ try {
103
+ let options = {timeoutMs: timeout->Option.getOr(60000)}
104
+ let parsed = await parse(source, ~options)
105
+ jsonToSpec(parsed)
106
+ } catch {
107
+ | JsExn(err) => {
108
+ let message = err->JsExn.message->Option.getOr("Unknown error parsing spec")
109
+ Error(`Failed to parse spec: ${message}`)
110
+ }
111
+ | _ => Error("Unknown error parsing spec")
112
+ }
113
+ }
114
+
115
+ // Bundle an OpenAPI spec (combines all external references into a single file)
116
+ let bundleSpec = async (source: string, ~timeout: option<int>=?): result<JSON.t, string> => {
117
+ try {
118
+ let options = {timeoutMs: timeout->Option.getOr(60000)}
119
+ let bundled = await bundle(source, ~options)
120
+ Ok(bundled)
121
+ } catch {
122
+ | JsExn(err) => {
123
+ let message = err->JsExn.message->Option.getOr("Unknown error bundling spec")
124
+ Error(`Failed to bundle spec: ${message}`)
125
+ }
126
+ | _ => Error("Unknown error bundling spec")
127
+ }
128
+ }
129
+
130
+ // Validate an OpenAPI spec
131
+ let validateSpec = async (source: string, ~timeout: option<int>=?): result<Types.openAPISpec, string> => {
132
+ try {
133
+ let options = {timeoutMs: timeout->Option.getOr(60000)}
134
+ let validated = await validate(source, ~options)
135
+ jsonToSpec(validated)
136
+ } catch {
137
+ | JsExn(err) => {
138
+ let message = err->JsExn.message->Option.getOr("Unknown error validating spec")
139
+ Error(`Failed to validate spec: ${message}`)
140
+ }
141
+ | _ => Error("Unknown error validating spec")
142
+ }
143
+ }
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // SchemaRegistry.res - Registry for resolving $ref references in OpenAPI specs
4
+ //
5
+ // This module provides a way to:
6
+ // 1. Store all component schemas from an OpenAPI spec
7
+ // 2. Resolve $ref strings to their underlying jsonSchema
8
+ // 3. Track which schemas are being visited to detect circular references
9
+
10
+ open Types
11
+
12
+ // The registry type
13
+ type t = {
14
+ // All schemas from components.schemas
15
+ schemas: Dict.t<jsonSchema>,
16
+ // Track visited schemas during resolution to detect cycles
17
+ mutable visiting: array<string>,
18
+ }
19
+
20
+ // Create a registry from an OpenAPI spec
21
+ let fromSpec = (spec: openAPISpec): t => {
22
+ let schemas = switch spec.components {
23
+ | None => Dict.make()
24
+ | Some(components) => {
25
+ switch components.schemas {
26
+ | None => Dict.make()
27
+ | Some(s) => s
28
+ }
29
+ }
30
+ }
31
+
32
+ {
33
+ schemas,
34
+ visiting: [],
35
+ }
36
+ }
37
+
38
+ // Create an empty registry
39
+ let empty = (): t => {
40
+ {
41
+ schemas: Dict.make(),
42
+ visiting: [],
43
+ }
44
+ }
45
+
46
+ // Extract schema name from a $ref string
47
+ // e.g., "#/components/schemas/Note" -> Some("Note")
48
+ let extractSchemaName = (ref: string): option<string> => {
49
+ let prefix = "#/components/schemas/"
50
+ if String.startsWith(ref, prefix) {
51
+ Some(String.slice(ref, ~start=String.length(prefix)))
52
+ } else {
53
+ None
54
+ }
55
+ }
56
+
57
+ // Check if a schema name is currently being visited (cycle detection)
58
+ let isVisiting = (registry: t, name: string): bool => {
59
+ registry.visiting->Array.includes(name)
60
+ }
61
+
62
+ // Start visiting a schema (for cycle detection)
63
+ let startVisiting = (registry: t, name: string): unit => {
64
+ registry.visiting = Array.concat(registry.visiting, [name])
65
+ }
66
+
67
+ // Stop visiting a schema
68
+ let stopVisiting = (registry: t, name: string): unit => {
69
+ registry.visiting = registry.visiting->Array.filter(n => n != name)
70
+ }
71
+
72
+ // Look up a schema by name
73
+ let getSchema = (registry: t, name: string): option<jsonSchema> => {
74
+ Dict.get(registry.schemas, name)
75
+ }
76
+
77
+ // Look up a schema by $ref string
78
+ let resolveRef = (registry: t, ref: string): option<jsonSchema> => {
79
+ switch extractSchemaName(ref) {
80
+ | None => None
81
+ | Some(name) => getSchema(registry, name)
82
+ }
83
+ }
84
+
85
+ // Get all schema names
86
+ let getSchemaNames = (registry: t): array<string> => {
87
+ Dict.keysToArray(registry.schemas)
88
+ }
89
+
90
+ // Check if a schema exists
91
+ let hasSchema = (registry: t, name: string): bool => {
92
+ Dict.get(registry.schemas, name)->Option.isSome
93
+ }
94
+
95
+ // Add a schema to the registry
96
+ let addSchema = (registry: t, name: string, schema: jsonSchema): unit => {
97
+ Dict.set(registry.schemas, name, schema)
98
+ }
99
+
100
+ // Merge schemas from another registry
101
+ let merge = (registry: t, other: t): unit => {
102
+ Dict.toArray(other.schemas)->Array.forEach(((name, schema)) => {
103
+ if !hasSchema(registry, name) {
104
+ addSchema(registry, name, schema)
105
+ }
106
+ })
107
+ }