@f3liz/rescript-autogen-openapi 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +111 -0
  3. package/lib/es6/src/Codegen.d.ts +28 -0
  4. package/lib/es6/src/Codegen.mjs +423 -0
  5. package/lib/es6/src/Types.d.ts +286 -0
  6. package/lib/es6/src/Types.mjs +20 -0
  7. package/lib/es6/src/bindings/Toposort.mjs +12 -0
  8. package/lib/es6/src/core/CodegenUtils.mjs +261 -0
  9. package/lib/es6/src/core/DocOverride.mjs +399 -0
  10. package/lib/es6/src/core/FileSystem.d.ts +4 -0
  11. package/lib/es6/src/core/FileSystem.mjs +78 -0
  12. package/lib/es6/src/core/IRBuilder.mjs +201 -0
  13. package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
  14. package/lib/es6/src/core/Pipeline.d.ts +6 -0
  15. package/lib/es6/src/core/Pipeline.mjs +150 -0
  16. package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
  17. package/lib/es6/src/core/Result.mjs +378 -0
  18. package/lib/es6/src/core/SchemaIR.mjs +425 -0
  19. package/lib/es6/src/core/SchemaIRParser.mjs +683 -0
  20. package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
  21. package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
  22. package/lib/es6/src/core/SpecDiffer.mjs +251 -0
  23. package/lib/es6/src/core/SpecMerger.mjs +237 -0
  24. package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +207 -0
  25. package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
  26. package/lib/es6/src/generators/EndpointGenerator.mjs +173 -0
  27. package/lib/es6/src/generators/IRToSuryGenerator.mjs +543 -0
  28. package/lib/es6/src/generators/IRToTypeGenerator.mjs +592 -0
  29. package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
  30. package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
  31. package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
  32. package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
  33. package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
  34. package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
  35. package/lib/es6/src/types/CodegenError.d.ts +66 -0
  36. package/lib/es6/src/types/CodegenError.mjs +79 -0
  37. package/lib/es6/src/types/Config.d.ts +31 -0
  38. package/lib/es6/src/types/Config.mjs +42 -0
  39. package/lib/es6/src/types/GenerationContext.mjs +47 -0
  40. package/package.json +53 -0
  41. package/rescript.json +26 -0
  42. package/src/Codegen.res +231 -0
  43. package/src/Types.res +222 -0
  44. package/src/bindings/Toposort.res +16 -0
  45. package/src/core/CodegenUtils.res +180 -0
  46. package/src/core/DocOverride.res +504 -0
  47. package/src/core/FileSystem.res +63 -0
  48. package/src/core/IRBuilder.res +66 -0
  49. package/src/core/OpenAPIParser.res +144 -0
  50. package/src/core/Pipeline.res +52 -0
  51. package/src/core/ReferenceResolver.res +41 -0
  52. package/src/core/Result.res +187 -0
  53. package/src/core/SchemaIR.res +291 -0
  54. package/src/core/SchemaIRParser.res +454 -0
  55. package/src/core/SchemaRefResolver.res +143 -0
  56. package/src/core/SchemaRegistry.res +107 -0
  57. package/src/core/SpecDiffer.res +270 -0
  58. package/src/core/SpecMerger.res +245 -0
  59. package/src/generators/ComponentSchemaGenerator.res +210 -0
  60. package/src/generators/DiffReportGenerator.res +152 -0
  61. package/src/generators/EndpointGenerator.res +176 -0
  62. package/src/generators/IRToSuryGenerator.res +386 -0
  63. package/src/generators/IRToTypeGenerator.res +423 -0
  64. package/src/generators/IRToTypeScriptGenerator.res +77 -0
  65. package/src/generators/ModuleGenerator.res +363 -0
  66. package/src/generators/SchemaCodeGenerator.res +84 -0
  67. package/src/generators/ThinWrapperGenerator.res +124 -0
  68. package/src/generators/TypeScriptDtsGenerator.res +193 -0
  69. package/src/generators/TypeScriptWrapperGenerator.res +166 -0
  70. package/src/types/CodegenError.res +85 -0
  71. package/src/types/Config.res +95 -0
  72. package/src/types/GenerationContext.res +56 -0
@@ -0,0 +1,454 @@
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
+ // Also handles OpenAPI 3.1 array form: type: ["string", "null"]
9
+ let parseTypeString = (rawType: Types.jsonSchemaType): Types.jsonSchemaType => {
10
+ // The rawType might actually be a raw string from JSON, so we need to handle that
11
+ // We use Obj.magic to get the underlying JS value and check it
12
+ let rawStr: string = Obj.magic(rawType)
13
+ switch rawStr {
14
+ | "string" => Types.String
15
+ | "number" => Types.Number
16
+ | "integer" => Types.Integer
17
+ | "boolean" => Types.Boolean
18
+ | "object" => Types.Object
19
+ | "null" => Types.Null
20
+ | "array" => Types.Array(Types.Unknown)
21
+ | _ => {
22
+ // It might already be a proper variant (like when recursively constructed)
23
+ // In that case, rawStr would be "String", "Number", etc.
24
+ switch rawStr {
25
+ | "String" => Types.String
26
+ | "Number" => Types.Number
27
+ | "Integer" => Types.Integer
28
+ | "Boolean" => Types.Boolean
29
+ | "Object" => Types.Object
30
+ | "Null" => Types.Null
31
+ | "Unknown" => Types.Unknown
32
+ | _ => Types.Unknown
33
+ }
34
+ }
35
+ }
36
+ }
37
+
38
+ // Check if the type field is an array (OpenAPI 3.1: type: ["string", "null"])
39
+ // Returns Some(array of parsed types) if array, None otherwise
40
+ let parseTypeAsArray = (rawType: Types.jsonSchemaType): option<array<Types.jsonSchemaType>> => {
41
+ let raw: 'a = Obj.magic(rawType)
42
+ if Array.isArray(raw) {
43
+ let arr: array<string> = Obj.magic(raw)
44
+ Some(arr->Array.map(s => {
45
+ let t: Types.jsonSchemaType = Obj.magic(s)
46
+ parseTypeString(t)
47
+ }))
48
+ } else {
49
+ None
50
+ }
51
+ }
52
+
53
+ // Parsing context to collect warnings
54
+ type parsingContext = {
55
+ mutable warnings: array<Types.warning>,
56
+ path: string,
57
+ }
58
+
59
+ let addWarning = (ctx: parsingContext, warning: Types.warning): unit => {
60
+ ctx.warnings->Array.push(warning)
61
+ }
62
+
63
+ // Convert JSON Schema to IR with depth limit to prevent infinite recursion
64
+ let rec parseJsonSchemaWithContext = (
65
+ ~ctx: parsingContext,
66
+ ~depth=0,
67
+ schema: Types.jsonSchema,
68
+ ): SchemaIR.irType => {
69
+ // Safety: Prevent infinite recursion on circular schemas
70
+ if depth > 30 {
71
+ addWarning(
72
+ ctx,
73
+ DepthLimitReached({
74
+ depth: depth,
75
+ path: ctx.path,
76
+ }),
77
+ )
78
+ SchemaIR.Unknown
79
+ } else {
80
+ // Handle $ref first
81
+ switch schema.ref {
82
+ | Some(ref) => SchemaIR.Reference(ref)
83
+ | None => {
84
+ // Check if nullable
85
+ let isNullable = schema.nullable->Option.getOr(false)
86
+
87
+ // Handle OpenAPI 3.1 array type: type: ["string", "null"]
88
+ // But skip if anyOf/oneOf is present — those are more specific
89
+ let hasComposition = schema.anyOf->Option.isSome || schema.oneOf->Option.isSome
90
+ let typeAsArray = if hasComposition {
91
+ None
92
+ } else {
93
+ schema.type_->Option.flatMap(parseTypeAsArray)
94
+ }
95
+ // When composition is preferred, check if the type_ is actually an array
96
+ // and clear it so the None branch runs the composition handlers
97
+ let schema = if hasComposition && schema.type_->Option.flatMap(parseTypeAsArray)->Option.isSome {
98
+ {...schema, type_: None}
99
+ } else {
100
+ schema
101
+ }
102
+
103
+ switch typeAsArray {
104
+ | Some(types) if Array.length(types) > 1 => {
105
+ // Array type form — convert to Union of parsed types
106
+ let irTypes = types->Array.map(t => {
107
+ let subSchema = {...schema, type_: Some(t), nullable: None}
108
+ parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, subSchema)
109
+ })
110
+ let baseType = SchemaIR.Union(irTypes)
111
+ if isNullable {
112
+ SchemaIR.Option(baseType)
113
+ } else {
114
+ baseType
115
+ }
116
+ }
117
+ | Some(types) if Array.length(types) == 1 => {
118
+ // Single-element array: treat as that type
119
+ let subSchema = {...schema, type_: Some(types->Array.getUnsafe(0)), nullable: None}
120
+ let baseType = parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, subSchema)
121
+ if isNullable { SchemaIR.Option(baseType) } else { baseType }
122
+ }
123
+ | _ => {
124
+
125
+ // Normalize the type field (raw JSON strings like "string" -> variant String)
126
+ let normalizedType = schema.type_->Option.map(parseTypeString)
127
+
128
+ // Parse base type
129
+ let baseType = switch normalizedType {
130
+ | Some(Types.String) => {
131
+ let constraints: SchemaIR.stringConstraints = {
132
+ minLength: schema.minLength,
133
+ maxLength: schema.maxLength,
134
+ pattern: schema.pattern,
135
+ }
136
+ SchemaIR.String({constraints: constraints})
137
+ }
138
+ | Some(Types.Number) => {
139
+ let constraints: SchemaIR.numberConstraints = {
140
+ minimum: schema.minimum,
141
+ maximum: schema.maximum,
142
+ multipleOf: None, // Not in jsonSchema type
143
+ }
144
+ SchemaIR.Number({constraints: constraints})
145
+ }
146
+ | Some(Types.Integer) => {
147
+ let constraints: SchemaIR.numberConstraints = {
148
+ minimum: schema.minimum,
149
+ maximum: schema.maximum,
150
+ multipleOf: None, // Not in jsonSchema type
151
+ }
152
+ SchemaIR.Integer({constraints: constraints})
153
+ }
154
+ | Some(Types.Boolean) => SchemaIR.Boolean
155
+ | Some(Types.Null) => SchemaIR.Null
156
+ | Some(Types.Array(_)) => {
157
+ let items = switch schema.items {
158
+ | None => SchemaIR.Unknown
159
+ | Some(itemSchema) =>
160
+ parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, itemSchema)
161
+ }
162
+
163
+ let constraints: SchemaIR.arrayConstraints = {
164
+ minItems: None, // Not in current jsonSchema type
165
+ maxItems: None, // Not in current jsonSchema type
166
+ uniqueItems: false,
167
+ }
168
+
169
+ SchemaIR.Array({items, constraints})
170
+ }
171
+ | Some(Types.Object) => {
172
+ // Check if this is an allOf composition (common in OpenAPI)
173
+ switch schema.allOf {
174
+ | Some(schemas) => {
175
+ // allOf with type: "object" - parse as intersection
176
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
177
+ SchemaIR.Intersection(types)
178
+ }
179
+ | None => {
180
+ // Regular object type - parse properties
181
+ let properties = switch schema.properties {
182
+ | None => []
183
+ | Some(propsDict) => {
184
+ let required = schema.required->Option.getOr([])
185
+ Dict.toArray(propsDict)->Array.map(((name, propSchema)) => {
186
+ let isRequired = required->Array.includes(name)
187
+ let propType = parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, propSchema)
188
+ (name, propType, isRequired)
189
+ })
190
+ }
191
+ }
192
+
193
+ // additionalProperties not in current jsonSchema type
194
+ let additionalProperties = None
195
+
196
+ SchemaIR.Object({
197
+ properties,
198
+ additionalProperties,
199
+ })
200
+ }
201
+ }
202
+ }
203
+ | Some(Types.Unknown) => SchemaIR.Unknown
204
+ | None => {
205
+ // No type specified, check for enum, properties, or combinators
206
+ switch (schema.enum, schema.properties, schema.allOf, schema.oneOf, schema.anyOf) {
207
+ | (Some(enumValues), _, _, _, _) => {
208
+ // Enum - convert to union of literals
209
+ let literals = enumValues->Array.map(value => {
210
+ switch value {
211
+ | String(str) => SchemaIR.Literal(SchemaIR.StringLiteral(str))
212
+ | Number(num) => SchemaIR.Literal(SchemaIR.NumberLiteral(num))
213
+ | Boolean(b) => SchemaIR.Literal(SchemaIR.BooleanLiteral(b))
214
+ | Null => SchemaIR.Literal(SchemaIR.NullLiteral)
215
+ | _ => SchemaIR.Unknown
216
+ }
217
+ })
218
+ SchemaIR.Union(literals)
219
+ }
220
+ | (_, Some(_), _, _, _) => {
221
+ // Has properties, treat as object (clear nullable to avoid double wrapping)
222
+ parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, {...schema, type_: Some(Object), nullable: None})
223
+ }
224
+ | (_, _, Some(schemas), _, _) => {
225
+ // allOf - intersection
226
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
227
+ SchemaIR.Intersection(types)
228
+ }
229
+ | (_, _, _, Some(schemas), _) => {
230
+ // oneOf - union
231
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
232
+ SchemaIR.Union(types)
233
+ }
234
+ | (_, _, _, _, Some(schemas)) => {
235
+ // anyOf - union
236
+ let types = schemas->Array.map(s => parseJsonSchemaWithContext(~ctx, ~depth=depth + 1, s))
237
+ SchemaIR.Union(types)
238
+ }
239
+ | _ => SchemaIR.Unknown
240
+ }
241
+ }
242
+ }
243
+
244
+ // Wrap in Option if nullable
245
+ if isNullable {
246
+ SchemaIR.Option(baseType)
247
+ } else {
248
+ baseType
249
+ }
250
+ } // end | _ => (typeAsArray arm)
251
+ } // end switch typeAsArray
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ // Convenience wrapper that creates a context and returns warnings
258
+ let parseJsonSchema = (~depth=0, schema: Types.jsonSchema): (SchemaIR.irType, array<Types.warning>) => {
259
+ let ctx = {warnings: [], path: "root"}
260
+ let irType = parseJsonSchemaWithContext(~ctx, ~depth, schema)
261
+ (irType, ctx.warnings)
262
+ }
263
+
264
+ // Parse a named schema
265
+ let parseNamedSchema = (~name: string, ~schema: Types.jsonSchema): (SchemaIR.namedSchema, array<Types.warning>) => {
266
+ let ctx = {warnings: [], path: `components.schemas.${name}`}
267
+ let type_ = parseJsonSchemaWithContext(~ctx, schema)
268
+ ({
269
+ name,
270
+ description: schema.description,
271
+ type_,
272
+ }, ctx.warnings)
273
+ }
274
+
275
+ // Normalize $ref paths in IR: "#/components/schemas/Name" → "Name" (when Name exists in available schemas)
276
+ let rec normalizeReferences = (~availableNames: array<string>, irType: SchemaIR.irType): SchemaIR.irType => {
277
+ switch irType {
278
+ | SchemaIR.Reference(ref) => {
279
+ let parts = ref->String.split("/")
280
+ let name = parts->Array.get(parts->Array.length - 1)->Option.getOr("")
281
+ if availableNames->Array.includes(name) {
282
+ SchemaIR.Reference(name)
283
+ } else {
284
+ irType
285
+ }
286
+ }
287
+ | SchemaIR.Array({items, constraints}) =>
288
+ SchemaIR.Array({items: normalizeReferences(~availableNames, items), constraints})
289
+ | SchemaIR.Object({properties, additionalProperties}) => {
290
+ let newProperties = properties->Array.map(((n, t, r)) =>
291
+ (n, normalizeReferences(~availableNames, t), r)
292
+ )
293
+ let newAdditional = additionalProperties->Option.map(t => normalizeReferences(~availableNames, t))
294
+ SchemaIR.Object({properties: newProperties, additionalProperties: newAdditional})
295
+ }
296
+ | SchemaIR.Union(types) =>
297
+ SchemaIR.Union(types->Array.map(t => normalizeReferences(~availableNames, t)))
298
+ | SchemaIR.Intersection(types) =>
299
+ SchemaIR.Intersection(types->Array.map(t => normalizeReferences(~availableNames, t)))
300
+ | SchemaIR.Option(inner) =>
301
+ SchemaIR.Option(normalizeReferences(~availableNames, inner))
302
+ | other => other
303
+ }
304
+ }
305
+
306
+ // Parse all component schemas
307
+ let parseComponentSchemas = (schemas: dict<Types.jsonSchema>): (SchemaIR.schemaContext, array<Types.warning>) => {
308
+ let namedSchemas = Dict.make()
309
+ let allWarnings = []
310
+
311
+ schemas->Dict.toArray->Array.forEach(((name, schema)) => {
312
+ let (namedSchema, warnings) = parseNamedSchema(~name, ~schema)
313
+ Dict.set(namedSchemas, name, namedSchema)
314
+ allWarnings->Array.pushMany(warnings)
315
+ })
316
+
317
+ // Resolve $ref paths in the IR: normalize "#/components/schemas/Name" to just "Name"
318
+ let availableNames = Dict.keysToArray(namedSchemas)
319
+ namedSchemas->Dict.toArray->Array.forEach(((name, namedSchema)) => {
320
+ let resolved = normalizeReferences(~availableNames, namedSchema.type_)
321
+ Dict.set(namedSchemas, name, {...namedSchema, type_: resolved})
322
+ })
323
+
324
+ ({schemas: namedSchemas}, allWarnings)
325
+ }
326
+
327
+ // Resolve a reference in the context
328
+ let resolveReference = (
329
+ ~context: SchemaIR.schemaContext,
330
+ ~ref: string,
331
+ ): option<SchemaIR.namedSchema> => {
332
+ // Handle #/components/schemas/SchemaName format
333
+ let parts = ref->String.split("/")
334
+ switch parts->Array.get(parts->Array.length - 1) {
335
+ | None => None
336
+ | Some(schemaName) => Dict.get(context.schemas, schemaName)
337
+ }
338
+ }
339
+
340
+ // Inline simple references to reduce indirection
341
+ let rec inlineSimpleReferences = (
342
+ ~context: SchemaIR.schemaContext,
343
+ ~irType: SchemaIR.irType,
344
+ ~depth: int=0,
345
+ ~maxDepth: int=2,
346
+ ): SchemaIR.irType => {
347
+ if depth >= maxDepth {
348
+ irType
349
+ } else {
350
+ switch irType {
351
+ | SchemaIR.Reference(ref) => {
352
+ switch resolveReference(~context, ~ref) {
353
+ | None => irType
354
+ | Some(schema) => {
355
+ // Only inline if it's a simple type
356
+ if SchemaIR.isSimpleType(schema.type_) {
357
+ inlineSimpleReferences(~context, ~irType=schema.type_, ~depth=depth + 1, ~maxDepth)
358
+ } else {
359
+ irType
360
+ }
361
+ }
362
+ }
363
+ }
364
+ | SchemaIR.Option(inner) =>
365
+ SchemaIR.Option(inlineSimpleReferences(~context, ~irType=inner, ~depth, ~maxDepth))
366
+ | SchemaIR.Array({items, constraints}) =>
367
+ SchemaIR.Array({
368
+ items: inlineSimpleReferences(~context, ~irType=items, ~depth, ~maxDepth),
369
+ constraints,
370
+ })
371
+ | SchemaIR.Object({properties, additionalProperties}) => {
372
+ let newProperties = properties->Array.map(((name, type_, required)) => {
373
+ (name, inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth), required)
374
+ })
375
+ let newAdditionalProps = additionalProperties->Option.map(type_ =>
376
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
377
+ )
378
+ SchemaIR.Object({
379
+ properties: newProperties,
380
+ additionalProperties: newAdditionalProps,
381
+ })
382
+ }
383
+ | SchemaIR.Union(types) =>
384
+ SchemaIR.Union(types->Array.map(type_ =>
385
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
386
+ ))
387
+ | SchemaIR.Intersection(types) =>
388
+ SchemaIR.Intersection(types->Array.map(type_ =>
389
+ inlineSimpleReferences(~context, ~irType=type_, ~depth, ~maxDepth)
390
+ ))
391
+ | other => other
392
+ }
393
+ }
394
+ }
395
+
396
+ // Optimize IR by simplifying unions, intersections, etc.
397
+ let rec optimizeIR = (irType: SchemaIR.irType): SchemaIR.irType => {
398
+ switch irType {
399
+ | SchemaIR.Union(types) => {
400
+ // Flatten nested unions
401
+ let flattened = types->Array.flatMap(t => {
402
+ switch optimizeIR(t) {
403
+ | SchemaIR.Union(inner) => inner
404
+ | other => [other]
405
+ }
406
+ })
407
+
408
+ // Remove duplicates (simple dedup by toString)
409
+ let unique = []
410
+ flattened->Array.forEach(type_ => {
411
+ let typeStr = SchemaIR.toString(type_)
412
+ let exists = unique->Array.some(t => SchemaIR.toString(t) == typeStr)
413
+ if !exists { unique->Array.push(type_) }
414
+ })
415
+
416
+ // Simplify single-element unions
417
+ switch unique {
418
+ | [] => SchemaIR.Unknown
419
+ | [single] => single
420
+ | multiple => SchemaIR.Union(multiple)
421
+ }
422
+ }
423
+ | SchemaIR.Intersection(types) => {
424
+ // Flatten nested intersections
425
+ let flattened = types->Array.flatMap(t => {
426
+ switch optimizeIR(t) {
427
+ | SchemaIR.Intersection(inner) => inner
428
+ | other => [other]
429
+ }
430
+ })
431
+
432
+ // Simplify single-element intersections
433
+ switch flattened {
434
+ | [] => SchemaIR.Unknown
435
+ | [single] => single
436
+ | multiple => SchemaIR.Intersection(multiple)
437
+ }
438
+ }
439
+ | SchemaIR.Option(inner) => SchemaIR.Option(optimizeIR(inner))
440
+ | SchemaIR.Array({items, constraints}) =>
441
+ SchemaIR.Array({items: optimizeIR(items), constraints})
442
+ | SchemaIR.Object({properties, additionalProperties}) => {
443
+ let newProperties = properties->Array.map(((name, type_, required)) => {
444
+ (name, optimizeIR(type_), required)
445
+ })
446
+ let newAdditionalProps = additionalProperties->Option.map(optimizeIR)
447
+ SchemaIR.Object({
448
+ properties: newProperties,
449
+ additionalProperties: newAdditionalProps,
450
+ })
451
+ }
452
+ | other => other
453
+ }
454
+ }
@@ -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
+ }