@f3liz/rescript-autogen-openapi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +339 -0
- package/README.md +98 -0
- package/lib/es6/src/Codegen.mjs +423 -0
- package/lib/es6/src/Types.mjs +20 -0
- package/lib/es6/src/core/CodegenUtils.mjs +186 -0
- package/lib/es6/src/core/DocOverride.mjs +399 -0
- package/lib/es6/src/core/FileSystem.mjs +78 -0
- package/lib/es6/src/core/IRBuilder.mjs +201 -0
- package/lib/es6/src/core/OpenAPIParser.mjs +168 -0
- package/lib/es6/src/core/Pipeline.mjs +150 -0
- package/lib/es6/src/core/ReferenceResolver.mjs +41 -0
- package/lib/es6/src/core/Result.mjs +378 -0
- package/lib/es6/src/core/SchemaIR.mjs +355 -0
- package/lib/es6/src/core/SchemaIRParser.mjs +490 -0
- package/lib/es6/src/core/SchemaRefResolver.mjs +146 -0
- package/lib/es6/src/core/SchemaRegistry.mjs +92 -0
- package/lib/es6/src/core/SpecDiffer.mjs +251 -0
- package/lib/es6/src/core/SpecMerger.mjs +237 -0
- package/lib/es6/src/generators/ComponentSchemaGenerator.mjs +125 -0
- package/lib/es6/src/generators/DiffReportGenerator.mjs +155 -0
- package/lib/es6/src/generators/EndpointGenerator.mjs +172 -0
- package/lib/es6/src/generators/IRToSuryGenerator.mjs +233 -0
- package/lib/es6/src/generators/IRToTypeGenerator.mjs +241 -0
- package/lib/es6/src/generators/IRToTypeScriptGenerator.mjs +143 -0
- package/lib/es6/src/generators/ModuleGenerator.mjs +285 -0
- package/lib/es6/src/generators/SchemaCodeGenerator.mjs +77 -0
- package/lib/es6/src/generators/ThinWrapperGenerator.mjs +97 -0
- package/lib/es6/src/generators/TypeScriptDtsGenerator.mjs +172 -0
- package/lib/es6/src/generators/TypeScriptWrapperGenerator.mjs +145 -0
- package/lib/es6/src/types/CodegenError.mjs +79 -0
- package/lib/es6/src/types/Config.mjs +42 -0
- package/lib/es6/src/types/GenerationContext.mjs +24 -0
- package/package.json +44 -0
- package/rescript.json +20 -0
- package/src/Codegen.res +222 -0
- package/src/Types.res +195 -0
- package/src/core/CodegenUtils.res +130 -0
- package/src/core/DocOverride.res +504 -0
- package/src/core/FileSystem.res +62 -0
- package/src/core/IRBuilder.res +66 -0
- package/src/core/OpenAPIParser.res +144 -0
- package/src/core/Pipeline.res +51 -0
- package/src/core/ReferenceResolver.res +41 -0
- package/src/core/Result.res +187 -0
- package/src/core/SchemaIR.res +258 -0
- package/src/core/SchemaIRParser.res +360 -0
- package/src/core/SchemaRefResolver.res +143 -0
- package/src/core/SchemaRegistry.res +107 -0
- package/src/core/SpecDiffer.res +270 -0
- package/src/core/SpecMerger.res +245 -0
- package/src/generators/ComponentSchemaGenerator.res +127 -0
- package/src/generators/DiffReportGenerator.res +152 -0
- package/src/generators/EndpointGenerator.res +172 -0
- package/src/generators/IRToSuryGenerator.res +199 -0
- package/src/generators/IRToTypeGenerator.res +199 -0
- package/src/generators/IRToTypeScriptGenerator.res +72 -0
- package/src/generators/ModuleGenerator.res +362 -0
- package/src/generators/SchemaCodeGenerator.res +83 -0
- package/src/generators/ThinWrapperGenerator.res +124 -0
- package/src/generators/TypeScriptDtsGenerator.res +193 -0
- package/src/generators/TypeScriptWrapperGenerator.res +166 -0
- package/src/types/CodegenError.res +82 -0
- package/src/types/Config.res +89 -0
- package/src/types/GenerationContext.res +23 -0
|
@@ -0,0 +1,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
|
+
}
|