@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,504 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // DocOverride.res - Handle documentation override from markdown files
4
+
5
+ // Generate hash of endpoint for change detection
6
+ let generateEndpointHash = (endpoint: Types.endpoint): string => {
7
+ let parts = [
8
+ endpoint.path,
9
+ endpoint.method,
10
+ endpoint.operationId->Option.getOr(""),
11
+ endpoint.summary->Option.getOr(""),
12
+ endpoint.description->Option.getOr(""),
13
+ ]
14
+
15
+ // Simple hash: just join and take first chars of each part
16
+ // In production, you might want to use a proper hash function
17
+ let combined = Array.join(parts, "|")
18
+ let hash = combined
19
+ ->String.split("")
20
+ ->Array.reduce(0, (acc, char) => {
21
+ let code = Js.String.charCodeAt(0, char)->Int.fromFloat
22
+ mod((acc->Int.shiftLeft(5)) - acc + code, 0x7FFFFFFF)
23
+ })
24
+ ->Int.toString(~radix=16)
25
+
26
+ hash
27
+ }
28
+
29
+ // Endpoint documentation metadata
30
+ type endpointDocMetadata = {
31
+ endpoint: string,
32
+ method: string,
33
+ hash: string,
34
+ host: option<string>,
35
+ version: option<string>,
36
+ operationId: option<string>,
37
+ }
38
+
39
+ // Validation result
40
+ type validationResult =
41
+ | Valid
42
+ | HashMismatch({expected: string, found: string})
43
+ | MissingFile
44
+ | ParseError(string)
45
+
46
+ // Parse markdown override file
47
+ type overrideContent = {
48
+ metadata: endpointDocMetadata,
49
+ defaultDescription: string,
50
+ overrideDescription: option<string>,
51
+ hasOverride: bool, // Whether user provided custom documentation
52
+ }
53
+
54
+ // Extract code block content from markdown between ```
55
+ let extractCodeBlock = (markdown: string): option<string> => {
56
+ // Find the first ``` block manually
57
+ let parts = markdown->String.split("```")
58
+
59
+ // We need at least 3 parts: [before, content, after]
60
+ if Array.length(parts) >= 3 {
61
+ switch parts->Array.get(1) {
62
+ | None => None
63
+ | Some(content) => {
64
+ let trimmed = content->String.trim
65
+
66
+ // Check if content is empty or placeholder
67
+ if trimmed == "" || trimmed == "<!-- Empty - no override -->" {
68
+ None
69
+ } else {
70
+ Some(trimmed)
71
+ }
72
+ }
73
+ }
74
+ } else {
75
+ None
76
+ }
77
+ }
78
+
79
+ // Parse override markdown file
80
+ let parseOverrideMarkdown = (content: string): option<overrideContent> => {
81
+ // Split into frontmatter and body
82
+ let parts = content->String.split("---")
83
+
84
+ if Array.length(parts) < 3 {
85
+ None
86
+ } else {
87
+ // Parse frontmatter (parts[1])
88
+ let frontmatter = parts->Array.get(1)->Option.getOr("")
89
+ let body = parts->Array.slice(~start=2, ~end=Array.length(parts))->Array.join("---")->String.trim
90
+
91
+ // Extract metadata from frontmatter
92
+ let lines = frontmatter->String.split("\n")->Array.map(String.trim)
93
+ let metadata = {
94
+ endpoint: lines
95
+ ->Array.find(l => l->String.startsWith("endpoint:"))
96
+ ->Option.map(l => {
97
+ let parts = l->String.split(":")
98
+ parts->Array.slice(~start=1, ~end=Array.length(parts))->Array.join(":")->String.trim
99
+ })
100
+ ->Option.getOr(""),
101
+ method: lines
102
+ ->Array.find(l => l->String.startsWith("method:"))
103
+ ->Option.map(l => l->String.split(":")->Array.get(1)->Option.getOr("")->String.trim)
104
+ ->Option.getOr(""),
105
+ hash: lines
106
+ ->Array.find(l => l->String.startsWith("hash:"))
107
+ ->Option.map(l => l->String.split(":")->Array.get(1)->Option.getOr("")->String.trim)
108
+ ->Option.getOr(""),
109
+ host: lines
110
+ ->Array.find(l => l->String.startsWith("host:"))
111
+ ->Option.flatMap(l => {
112
+ let parts = l->String.split(":")
113
+ parts->Array.slice(~start=1, ~end=Array.length(parts))->Array.join(":")->String.trim->Some
114
+ }),
115
+ version: lines
116
+ ->Array.find(l => l->String.startsWith("version:"))
117
+ ->Option.flatMap(l => l->String.split(":")->Array.get(1)->Option.map(String.trim)),
118
+ operationId: lines
119
+ ->Array.find(l => l->String.startsWith("operationId:"))
120
+ ->Option.flatMap(l => l->String.split(":")->Array.get(1)->Option.map(String.trim)),
121
+ }
122
+
123
+ // Extract default description and override
124
+ let defaultDescSection = body
125
+ ->String.split("## Override")
126
+ ->Array.get(0)
127
+ ->Option.getOr("")
128
+ ->String.split("## Default Description")
129
+ ->{parts => parts->Array.slice(~start=1, ~end=Array.length(parts))}
130
+ ->Array.join("## Default Description")
131
+ ->String.trim
132
+
133
+ let overrideSection = body
134
+ ->String.split("## Override")
135
+ ->{parts => parts->Array.slice(~start=1, ~end=Array.length(parts))}
136
+ ->Array.join("## Override")
137
+
138
+ let overrideDesc = extractCodeBlock(overrideSection)
139
+ let hasOverride = overrideDesc->Option.isSome
140
+
141
+ Some({
142
+ metadata,
143
+ defaultDescription: defaultDescSection,
144
+ overrideDescription: overrideDesc,
145
+ hasOverride,
146
+ })
147
+ }
148
+ }
149
+
150
+ // Generate markdown override file content
151
+ let generateOverrideMarkdown = (
152
+ ~endpoint: Types.endpoint,
153
+ ~host: option<string>=?,
154
+ ~version: option<string>=?,
155
+ ()
156
+ ): string => {
157
+ let hash = generateEndpointHash(endpoint)
158
+ let operationName = CodegenUtils.generateOperationName(
159
+ endpoint.operationId,
160
+ endpoint.path,
161
+ endpoint.method,
162
+ )
163
+
164
+ let defaultDesc = switch (endpoint.summary, endpoint.description) {
165
+ | (None, None) => "No description provided."
166
+ | (Some(s), None) => s
167
+ | (None, Some(d)) => d
168
+ | (Some(s), Some(d)) if s == d => s
169
+ | (Some(s), Some(d)) => s ++ "\n\n" ++ d
170
+ }
171
+
172
+ let metadata = [
173
+ "---",
174
+ `endpoint: ${endpoint.path}`,
175
+ `method: ${endpoint.method->String.toUpperCase}`,
176
+ `hash: ${hash}`,
177
+ ]->Array.concat(
178
+ [
179
+ host->Option.map(h => `host: ${h}`),
180
+ version->Option.map(v => `version: ${v}`),
181
+ endpoint.operationId->Option.map(id => `operationId: ${id}`),
182
+ Some("---"),
183
+ ]->Array.filterMap(x => x)
184
+ )
185
+
186
+ let content = `
187
+ |${Array.join(metadata, "\n")}
188
+ |
189
+ |# ${endpoint.summary->Option.getOr(endpoint.path)}
190
+ |
191
+ |**Path**: \`${endpoint.path}\`
192
+ |**Method**: \`${endpoint.method->String.toUpperCase}\`
193
+ |**Operation**: \`${operationName}\`
194
+ |
195
+ |## Default Description
196
+ |
197
+ |${defaultDesc}
198
+ |
199
+ |## Override
200
+ |
201
+ |Add your custom documentation here. If this code block is empty, the default description will be used.
202
+ |
203
+ |\`\`\`
204
+ |<!-- Empty - no override -->
205
+ |\`\`\`
206
+ |`
207
+
208
+ content->CodegenUtils.trimMargin
209
+ }
210
+
211
+ // Read override from file system
212
+ @module("fs") external existsSync: string => bool = "existsSync"
213
+ @module("fs") external readFileSync: (string, {..}) => string = "readFileSync"
214
+
215
+ // Validate override hash against current endpoint
216
+ let validateOverride = (
217
+ override: overrideContent,
218
+ currentHash: string,
219
+ ): validationResult => {
220
+ if override.metadata.hash == currentHash {
221
+ Valid
222
+ } else {
223
+ HashMismatch({expected: currentHash, found: override.metadata.hash})
224
+ }
225
+ }
226
+
227
+ // Read and validate override with hash checking
228
+ type readResult =
229
+ | NoOverride
230
+ | ValidOverride(string)
231
+ | InvalidHash({override: string, expected: string, found: string})
232
+ | FileError(string)
233
+
234
+ let readOverrideWithValidation = (
235
+ overrideDir: string,
236
+ moduleName: string,
237
+ functionName: string,
238
+ currentHash: string,
239
+ ): readResult => {
240
+ let filePath = FileSystem.makePath(
241
+ FileSystem.makePath(overrideDir, moduleName),
242
+ functionName ++ ".md"
243
+ )
244
+
245
+ if existsSync(filePath) {
246
+ try {
247
+ let content = readFileSync(filePath, {"encoding": "utf8"})
248
+ let parsed = parseOverrideMarkdown(content)
249
+
250
+ switch parsed {
251
+ | None => FileError("Failed to parse markdown file")
252
+ | Some(override) => {
253
+ // Check if user provided override
254
+ if !override.hasOverride {
255
+ NoOverride
256
+ } else {
257
+ // Validate hash
258
+ switch validateOverride(override, currentHash) {
259
+ | Valid => {
260
+ switch override.overrideDescription {
261
+ | None => NoOverride
262
+ | Some(desc) => ValidOverride(desc)
263
+ }
264
+ }
265
+ | HashMismatch({expected, found}) => {
266
+ switch override.overrideDescription {
267
+ | None => NoOverride
268
+ | Some(desc) => InvalidHash({override: desc, expected, found})
269
+ }
270
+ }
271
+ | MissingFile => FileError("Override file missing")
272
+ | ParseError(msg) => FileError(msg)
273
+ }
274
+ }
275
+ }
276
+ }
277
+ } catch {
278
+ | JsExn(err) => FileError(err->JsExn.message->Option.getOr("Unknown error"))
279
+ | _ => FileError("Unknown error reading file")
280
+ }
281
+ } else {
282
+ NoOverride
283
+ }
284
+ }
285
+
286
+ // Simple read for backward compatibility (no validation)
287
+ let readOverride = (overrideDir: string, moduleName: string, functionName: string): option<string> => {
288
+ let filePath = FileSystem.makePath(
289
+ FileSystem.makePath(overrideDir, moduleName),
290
+ functionName ++ ".md"
291
+ )
292
+
293
+ if existsSync(filePath) {
294
+ try {
295
+ let content = readFileSync(filePath, {"encoding": "utf8"})
296
+ let parsed = parseOverrideMarkdown(content)
297
+
298
+ switch parsed {
299
+ | None => None
300
+ | Some(override) => override.overrideDescription
301
+ }
302
+ } catch {
303
+ | _ => None
304
+ }
305
+ } else {
306
+ None
307
+ }
308
+ }
309
+
310
+ // Check if an override file has been customized (has user content in override section)
311
+ let isFileCustomized = (filePath: string): bool => {
312
+ if !existsSync(filePath) {
313
+ false
314
+ } else {
315
+ try {
316
+ let content = readFileSync(filePath, {"encoding": "utf8"})
317
+ let parsed = parseOverrideMarkdown(content)
318
+
319
+ switch parsed {
320
+ | None => false
321
+ | Some(override) => override.hasOverride // true if user added custom content
322
+ }
323
+ } catch {
324
+ | _ => false // If we can't read it, assume not customized
325
+ }
326
+ }
327
+ }
328
+
329
+ // Generate all override markdown files for an API spec
330
+ // IMPORTANT: This will NOT overwrite files that already exist with custom content
331
+ let generateOverrideFiles = (
332
+ ~spec: Types.openAPISpec,
333
+ ~endpoints: array<Types.endpoint>,
334
+ ~outputDir: string,
335
+ ~host: option<string>=?,
336
+ ~groupByTag: bool=true, // Whether to organize by tags or use flat structure
337
+ ()
338
+ ): array<FileSystem.fileToWrite> => {
339
+ let version = Some(spec.info.version)
340
+ let hostUrl = switch host {
341
+ | Some(h) => Some(h)
342
+ | None => spec.info.description
343
+ }
344
+
345
+ endpoints
346
+ ->Array.map(endpoint => {
347
+ let moduleName = if groupByTag {
348
+ switch endpoint.tags {
349
+ | Some(tags) => tags->Array.get(0)->Option.getOr("Default")
350
+ | None => "Default"
351
+ }
352
+ } else {
353
+ "API" // Flat structure - all in API module
354
+ }
355
+
356
+ let functionName = CodegenUtils.generateOperationName(
357
+ endpoint.operationId,
358
+ endpoint.path,
359
+ endpoint.method,
360
+ )
361
+
362
+ let modulePath = FileSystem.makePath(outputDir, CodegenUtils.toPascalCase(moduleName))
363
+ let filePath = FileSystem.makePath(modulePath, functionName ++ ".md")
364
+
365
+ // Check if file already exists with custom content
366
+ if isFileCustomized(filePath) {
367
+ // File has been customized - skip it
368
+ Console.log(`ℹ️ Skipping ${moduleName}/${functionName} - file already customized`)
369
+ None
370
+ } else {
371
+ // File doesn't exist or has no custom content - generate it
372
+ let content = generateOverrideMarkdown(
373
+ ~endpoint,
374
+ ~host=?hostUrl,
375
+ ~version=?version,
376
+ ()
377
+ )
378
+
379
+ Some({
380
+ FileSystem.path: filePath,
381
+ content: content,
382
+ })
383
+ }
384
+ })
385
+ ->Array.keepSome
386
+ }
387
+
388
+ // Generate README for override directory
389
+ let generateOverrideReadme = (~host: option<string>=?, ~version: option<string>=?, ()): string => {
390
+ let hostInfo = host->Option.getOr("Not specified")
391
+ let versionInfo = version->Option.getOr("Not specified")
392
+
393
+ `
394
+ |# API Documentation Overrides
395
+ |
396
+ |This directory contains markdown files that allow you to override the auto-generated documentation.
397
+ |
398
+ |## Global Information
399
+ |
400
+ |- **Host**: ${hostInfo}
401
+ |- **Version**: ${versionInfo}
402
+ |
403
+ |## Structure
404
+ |
405
+ |Each module has its own directory, and each endpoint has its own markdown file:
406
+ |
407
+ |\`\`\`
408
+ |docs/
409
+ |├── README.md (this file)
410
+ |├── Account/
411
+ |│ ├── postBlockingCreate.md
412
+ |│ ├── postBlockingDelete.md
413
+ |│ └── ...
414
+ |├── Notes/
415
+ |│ ├── postNotesCreate.md
416
+ |│ └── ...
417
+ |└── ...
418
+ |\`\`\`
419
+ |
420
+ |## How to Override
421
+ |
422
+ |1. Find the endpoint you want to document in its module directory
423
+ |2. Open the markdown file
424
+ |3. Edit the code block under the "## Override" section
425
+ |4. Add your custom documentation (supports markdown)
426
+ |5. Regenerate the code - your custom documentation will be used instead of the default
427
+ |
428
+ |## File Format
429
+ |
430
+ |Each file contains:
431
+ |
432
+ |### Frontmatter
433
+ |- \`endpoint\`: The API endpoint path
434
+ |- \`method\`: HTTP method (GET, POST, etc.)
435
+ |- \`hash\`: Hash of the endpoint for change detection
436
+ |- \`host\`: API host URL
437
+ |- \`version\`: API version
438
+ |- \`operationId\`: OpenAPI operation ID
439
+ |
440
+ |### Default Description
441
+ |The original description from the OpenAPI spec.
442
+ |
443
+ |### Override Section
444
+ |A code block where you can add your custom documentation. If empty, the default description is used.
445
+ |
446
+ |## Example
447
+ |
448
+ |\`\`\`markdown
449
+ |---
450
+ |endpoint: /blocking/create
451
+ |method: POST
452
+ |hash: abc123
453
+ |host: https://misskey.io
454
+ |version: 1.0.0
455
+ |---
456
+ |
457
+ |# blocking/create
458
+ |
459
+ |**Path**: \`/blocking/create\`
460
+ |**Method**: \`POST\`
461
+ |
462
+ |## Default Description
463
+ |
464
+ |No description provided.
465
+ |
466
+ |**Credential required**: *Yes* / **Permission**: *write:blocks*
467
+ |
468
+ |## Override
469
+ |
470
+ |\`\`\`
471
+ |Create a blocking relationship with another user.
472
+ |
473
+ |This endpoint allows you to block a user by their user ID. Once blocked:
474
+ |- The user will not be able to see your posts
475
+ |- You will not see their posts in your timeline
476
+ |- They cannot follow you
477
+ |
478
+ |**Parameters:**
479
+ |- \`userId\`: The ID of the user to block
480
+ |
481
+ |**Example:**
482
+ |\`\`\`typescript
483
+ |await client.blocking.create({ userId: "user123" })
484
+ |\`\`\`
485
+ |\`\`\`
486
+ |\`\`\`
487
+ |
488
+ |## Notes
489
+ |
490
+ |- The hash is used to detect if the endpoint has changed in the OpenAPI spec
491
+ |- If the endpoint changes, you may need to update your override
492
+ |- Empty override blocks (with just \`<!-- Empty - no override -->\`) are ignored
493
+ |`->CodegenUtils.trimMargin
494
+ }
495
+
496
+ // No automatic refresh - users should manually delete outdated files
497
+ // This is safer and forces users to review changes via git diff
498
+ //
499
+ // When an endpoint changes:
500
+ // 1. User gets a hash mismatch warning
501
+ // 2. User checks git diff to see their custom documentation
502
+ // 3. User deletes the outdated override file
503
+ // 4. User regenerates to get new template
504
+ // 5. User re-adds their custom documentation (reviewing if it's still valid)
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // FileSystem.res - File system operations (side effects isolated)
4
+
5
+ @module("fs") external mkdirSync: (string, {"recursive": bool}) => unit = "mkdirSync"
6
+ @module("fs") external writeFileSync: (string, string, string) => unit = "writeFileSync"
7
+ @module("@std/path") external join: (string, string) => string = "join"
8
+ @module("@std/path") external dirname: string => string = "dirname"
9
+
10
+ // Represents a file to be written
11
+ type fileToWrite = {
12
+ path: string,
13
+ content: string,
14
+ }
15
+
16
+ // Ensure directory exists
17
+ let ensureDir = (path: string): unit => {
18
+ try {
19
+ mkdirSync(dirname(path), {"recursive": true})
20
+ } catch {
21
+ | _ => ()
22
+ }
23
+ }
24
+
25
+ // Write a single file to disk
26
+ let writeFile = (file: fileToWrite): result<unit, string> => {
27
+ try {
28
+ ensureDir(file.path)
29
+ writeFileSync(file.path, file.content, "utf8")
30
+ Ok()
31
+ } catch {
32
+ | JsExn(exn) => {
33
+ let message = exn->JsExn.message->Option.getOr("Unknown error")
34
+ Error(`Failed to write file ${file.path}: ${message}`)
35
+ }
36
+ | _ => Error(`Failed to write file ${file.path}: Unknown error`)
37
+ }
38
+ }
39
+
40
+ // Write multiple files to disk
41
+ let writeFiles = (files: array<fileToWrite>): result<array<string>, array<string>> => {
42
+ let successes = []
43
+ let errors = []
44
+
45
+ files->Array.forEach(file => {
46
+ switch writeFile(file) {
47
+ | Ok() => successes->Array.push(file.path)
48
+ | Error(err) => errors->Array.push(err)
49
+ }
50
+ })
51
+
52
+ if Array.length(errors) > 0 {
53
+ Error(errors)
54
+ } else {
55
+ Ok(successes)
56
+ }
57
+ }
58
+
59
+ // Helper to create a file path
60
+ let makePath = (baseDir: string, filename: string): string => {
61
+ join(baseDir, filename)
62
+ }
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // IRBuilder.res - Fluent API for constructing IR types
4
+
5
+ // Constraint builders
6
+ module Constraints = {
7
+ let string = (~min=?, ~max=?, ~pattern=?, ()) => {
8
+ SchemaIR.minLength: min,
9
+ maxLength: max,
10
+ pattern,
11
+ }
12
+
13
+ let number = (~min=?, ~max=?, ~multipleOf=?, ()) => {
14
+ SchemaIR.minimum: min,
15
+ maximum: max,
16
+ multipleOf,
17
+ }
18
+
19
+ let array = (~min=?, ~max=?, ~unique=false, ()) => {
20
+ SchemaIR.minItems: min,
21
+ maxItems: max,
22
+ uniqueItems: unique,
23
+ }
24
+ }
25
+
26
+ // Type builders
27
+ let string = (~min=?, ~max=?, ~pattern=?, ()) =>
28
+ SchemaIR.String({constraints: Constraints.string(~min?, ~max?, ~pattern?, ())})
29
+
30
+ let number = (~min=?, ~max=?, ~multipleOf=?, ()) =>
31
+ SchemaIR.Number({constraints: Constraints.number(~min?, ~max?, ~multipleOf?, ())})
32
+
33
+ let int = (~min=?, ~max=?, ~multipleOf=?, ()) =>
34
+ SchemaIR.Integer({constraints: Constraints.number(~min?, ~max?, ~multipleOf?, ())})
35
+
36
+ let bool = SchemaIR.Boolean
37
+ let null = SchemaIR.Null
38
+ let unknown = SchemaIR.Unknown
39
+
40
+ let array = (~items, ~min=?, ~max=?, ~unique=false, ()) =>
41
+ SchemaIR.Array({items, constraints: Constraints.array(~min?, ~max?, ~unique, ())})
42
+
43
+ let object_ = (~props, ~additional=?, ()) =>
44
+ SchemaIR.Object({properties: props, additionalProperties: additional})
45
+
46
+ let union = types => SchemaIR.Union(types)
47
+ let intersection = types => SchemaIR.Intersection(types)
48
+ let ref = refPath => SchemaIR.Reference(refPath)
49
+ let option = type_ => SchemaIR.Option(type_)
50
+
51
+ // Literal builders
52
+ let stringLit = s => SchemaIR.Literal(StringLiteral(s))
53
+ let numberLit = n => SchemaIR.Literal(NumberLiteral(n))
54
+ let boolLit = b => SchemaIR.Literal(BooleanLiteral(b))
55
+ let nullLit = SchemaIR.Literal(NullLiteral)
56
+
57
+ // Property builder (name, type, required)
58
+ let prop = (name, type_, ~required=true, ()) => (name, type_, required)
59
+ let optProp = (name, type_) => (name, type_, false)
60
+
61
+ // Named schema builder
62
+ let named = (~name, ~description=?, type_) => {
63
+ SchemaIR.name,
64
+ description,
65
+ type_,
66
+ }