@f3liz/rescript-autogen-openapi 0.5.4 → 0.7.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.
@@ -1,74 +1,51 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Pipeline from "../core/Pipeline.mjs";
4
+ import * as Templates from "../core/Templates.mjs";
4
5
  import * as FileSystem from "../core/FileSystem.mjs";
6
+ import * as Handlebars from "../bindings/Handlebars.mjs";
5
7
  import * as CodegenUtils from "../core/CodegenUtils.mjs";
6
8
  import * as OpenAPIParser from "../core/OpenAPIParser.mjs";
7
9
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
10
  import * as JsConvertCase from "js-convert-case";
9
11
 
10
- let misskeyClientJsCode = CodegenUtils.trimMargin(`
11
- |export class MisskeyClient {
12
- | constructor(baseUrl, token) {
13
- | this.baseUrl = baseUrl;
14
- | this.token = token;
15
- | }
16
- |
17
- | async _fetch(url, method, body) {
18
- | const headers = { 'Content-Type': 'application/json' };
19
- | if (this.token) {
20
- | headers['Authorization'] = \`Bearer \${this.token}\`;
21
- | }
22
- | const response = await fetch(this.baseUrl + url, {
23
- | method,
24
- | headers,
25
- | body: body ? JSON.stringify(body) : undefined,
26
- | });
27
- | return response.json();
28
- | }
29
- |}`, undefined);
12
+ let body = "export class MisskeyClient {\n constructor(baseUrl, token) {\n this.baseUrl = baseUrl;\n this.token = token;\n }\n\n async _fetch(url, method, body) {\n const headers = { 'Content-Type': 'application/json' };\n if (this.token) {\n headers['Authorization'] = \x60Bearer \x24{this.token}\x60;\n }\n const response = await fetch(this.baseUrl + url, {\n method,\n headers,\n body: body ? JSON.stringify(body) : undefined,\n });\n return response.json();\n }\n}";
30
13
 
31
14
  function generateWrapperMjs(endpoints, generatedModulePath) {
32
15
  let endpointsByTag = OpenAPIParser.groupByTag(endpoints);
33
16
  let tags = Object.keys(endpointsByTag);
34
- let imports = tags.map(tag => {
35
- let moduleName = JsConvertCase.toPascalCase(tag);
36
- return `import * as ` + moduleName + ` from '` + generatedModulePath + `/` + moduleName + `.mjs';`;
37
- }).join("\n");
38
- let wrappers = tags.map(tag => {
17
+ let tagData = tags.map(tag => {
39
18
  let moduleName = JsConvertCase.toPascalCase(tag);
19
+ let importLine = `import * as ` + moduleName + ` from '` + generatedModulePath + `/` + moduleName + `.mjs';`;
40
20
  let methods = Stdlib_Option.getOr(endpointsByTag[tag], []).map(endpoint => {
41
21
  let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method);
42
22
  let hasRequestBody = Stdlib_Option.isSome(endpoint.requestBody);
43
- let bodyArg = hasRequestBody ? "body: request, " : "";
44
- return `
45
- | async ` + functionName + `(client` + (
46
- hasRequestBody ? ", request" : ""
47
- ) + `) {
48
- | return ` + moduleName + `.` + functionName + `({
49
- | ` + bodyArg + `fetch: (url, method, body) => client._fetch(url, method, body)
50
- | });
51
- | },`;
23
+ return Handlebars.render(Templates.wrapperMjsMethod, {
24
+ functionName: functionName,
25
+ moduleName: moduleName,
26
+ requestArg: hasRequestBody ? ", request" : "",
27
+ bodyArg: hasRequestBody ? "body: request, " : ""
28
+ });
52
29
  }).join("\n");
53
- return `
54
- |export const ` + moduleName + ` = {
55
- |` + methods + `
56
- |};`;
57
- }).join("\n\n");
58
- return CodegenUtils.trimMargin(`
59
- |// Generated wrapper
60
- |` + imports + `
61
- |
62
- |` + misskeyClientJsCode + `
63
- |
64
- |` + wrappers + `
65
- |`, undefined);
30
+ let namespace = Handlebars.render(Templates.wrapperMjsNamespace, {
31
+ moduleName: moduleName,
32
+ methods: methods
33
+ });
34
+ return {
35
+ importLine: importLine,
36
+ namespace: namespace
37
+ };
38
+ });
39
+ return Handlebars.render(Templates.wrapperMjs, {
40
+ tags: tagData,
41
+ clientCode: body
42
+ });
66
43
  }
67
44
 
68
45
  function generateWrapperDts(endpoints) {
69
46
  let endpointsByTag = OpenAPIParser.groupByTag(endpoints);
70
47
  let tags = Object.keys(endpointsByTag);
71
- let imports = tags.map(tag => {
48
+ let tagData = tags.map(tag => {
72
49
  let moduleName = JsConvertCase.toPascalCase(tag);
73
50
  let typesToImport = Stdlib_Option.getOr(endpointsByTag[tag], []).flatMap(endpoint => {
74
51
  let pascalName = JsConvertCase.toPascalCase(CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method));
@@ -81,45 +58,62 @@ function generateWrapperDts(endpoints) {
81
58
  return [` ` + pascalName + `Response,`];
82
59
  }
83
60
  }).join("\n");
84
- return `import type {
85
- ` + typesToImport + `
86
- } from '../types/` + moduleName + `.d.ts';`;
87
- }).join("\n");
88
- let namespaces = tags.map(tag => {
89
- let moduleName = JsConvertCase.toPascalCase(tag);
61
+ let importBlock = `import type {\n` + typesToImport + `\n} from '../types/` + moduleName + `.d.ts';`;
90
62
  let functions = Stdlib_Option.getOr(endpointsByTag[tag], []).map(endpoint => {
91
63
  let functionName = CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method);
92
64
  let pascalName = JsConvertCase.toPascalCase(functionName);
93
- let docComment = Stdlib_Option.mapOr(endpoint.summary, "", summary => {
94
- let descriptionPart = Stdlib_Option.mapOr(endpoint.description, "", description => {
95
- if (description === summary) {
96
- return "";
65
+ let match = endpoint.summary;
66
+ let match$1 = endpoint.description;
67
+ let docComment;
68
+ if (match !== undefined) {
69
+ if (match$1 !== undefined) {
70
+ if (match === match$1) {
71
+ docComment = ` /** ` + match + ` */\n`;
72
+ } else {
73
+ let descLines = match$1.split("\n").map(line => {
74
+ if (line === "") {
75
+ return " *";
76
+ } else {
77
+ return ` * ` + line;
78
+ }
79
+ });
80
+ docComment = ` /**\n * ` + match + `\n *\n` + descLines.join("\n") + `\n */\n`;
81
+ }
82
+ } else {
83
+ docComment = ` /** ` + match + ` */\n`;
84
+ }
85
+ } else if (match$1 !== undefined) {
86
+ let lines = match$1.split("\n").map(line => {
87
+ if (line === "") {
88
+ return " *";
97
89
  } else {
98
- return " - " + description;
90
+ return ` * ` + line;
99
91
  }
100
92
  });
101
- return ` /** ` + summary + descriptionPart + ` */\n`;
102
- });
93
+ docComment = ` /**\n` + lines.join("\n") + `\n */\n`;
94
+ } else {
95
+ docComment = "";
96
+ }
103
97
  let requestParam = Stdlib_Option.isSome(endpoint.requestBody) ? `, request: ` + pascalName + `Request` : "";
104
- return docComment + ` export function ` + functionName + `(client: MisskeyClient` + requestParam + `): Promise<` + pascalName + `Response>;`;
98
+ return Handlebars.render(Templates.wrapperDtsFunction, {
99
+ docComment: docComment,
100
+ functionName: functionName,
101
+ requestParam: requestParam,
102
+ pascalName: pascalName
103
+ });
105
104
  }).join("\n");
106
- return `
107
- |export namespace ` + moduleName + ` {
108
- |` + functions + `
109
- |}`;
110
- }).join("\n\n");
111
- return CodegenUtils.trimMargin(`
112
- |// Generated TypeScript definitions for wrapper
113
- |` + imports + `
114
- |
115
- |export class MisskeyClient {
116
- | constructor(baseUrl: string, token?: string);
117
- | readonly baseUrl: string;
118
- | readonly token?: string;
119
- |}
120
- |
121
- |` + namespaces + `
122
- |`, undefined);
105
+ let namespace = Handlebars.render(Templates.wrapperDtsNamespace, {
106
+ moduleName: moduleName,
107
+ functions: functions
108
+ });
109
+ return {
110
+ importBlock: importBlock,
111
+ namespace: namespace
112
+ };
113
+ });
114
+ return Handlebars.render(Templates.wrapperDts, {
115
+ tags: tagData
116
+ });
123
117
  }
124
118
 
125
119
  function generate(endpoints, outputDir, generatedModulePathOpt) {
@@ -136,10 +130,12 @@ function generate(endpoints, outputDir, generatedModulePathOpt) {
136
130
  ], []);
137
131
  }
138
132
 
133
+ let misskeyClientJsCode = body;
134
+
139
135
  export {
140
136
  misskeyClientJsCode,
141
137
  generateWrapperMjs,
142
138
  generateWrapperDts,
143
139
  generate,
144
140
  }
145
- /* misskeyClientJsCode Not a pure module */
141
+ /* FileSystem Not a pure module */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f3liz/rescript-autogen-openapi",
3
- "version": "0.5.4",
3
+ "version": "0.7.0",
4
4
  "description": "Generate ReScript code with Sury schemas from OpenAPI 3.1 specs. Supports multiple forks with diff/merge capabilities.",
5
5
  "keywords": [
6
6
  "rescript",
@@ -37,6 +37,7 @@
37
37
  "type": "module",
38
38
  "dependencies": {
39
39
  "@readme/openapi-parser": "^5.5.0",
40
+ "handlebars": "^4.7.8",
40
41
  "js-convert-case": "^4.2.0",
41
42
  "pathe": "^2.0.3",
42
43
  "toposort": "^2.0.2"
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ // Handlebars.res - Minimal Handlebars binding via %raw
4
+
5
+ @module("module") external createRequire: string => string => 'a = "createRequire"
6
+ @val @scope("import.meta") external importMetaUrl: string = "url"
7
+
8
+ let _require = createRequire(importMetaUrl)
9
+
10
+ // Internal: render with untyped data (JSON.t used as universal type at boundary)
11
+ let _render: (string, JSON.t) => string = {
12
+ let handlebars: 'a = _require("handlebars")
13
+ let convert: 'a = _require("js-convert-case")
14
+
15
+ %raw(`
16
+ (function(Handlebars, convert) {
17
+ var instance = Handlebars.create();
18
+
19
+ instance.registerHelper('indent', function(content, level) {
20
+ if (typeof content !== 'string') return '';
21
+ var spaces = ' '.repeat(typeof level === 'number' ? level : 1);
22
+ return content.split('\n').map(function(line) {
23
+ return line.trim() === '' ? '' : spaces + line;
24
+ }).join('\n');
25
+ });
26
+ instance.registerHelper('pascalCase', function(s) { return typeof s === 'string' ? convert.toPascalCase(s) : ''; });
27
+ instance.registerHelper('camelCase', function(s) { return typeof s === 'string' ? convert.toCamelCase(s) : ''; });
28
+ instance.registerHelper('upperCase', function(s) { return typeof s === 'string' ? s.toUpperCase() : ''; });
29
+ instance.registerHelper('eq', function(a, b) { return a === b; });
30
+ instance.registerHelper('ne', function(a, b) { return a !== b; });
31
+
32
+ var cache = {};
33
+ return function render(template, data) {
34
+ if (!cache[template]) cache[template] = instance.compile(template, { noEscape: true });
35
+ return cache[template](data);
36
+ };
37
+ })
38
+ `)(handlebars, convert)
39
+ }
40
+
41
+ // Public render: accepts any ReScript record/object via Obj.magic
42
+ let render = (template: string, data: 'a): string =>
43
+ _render(template, Obj.magic(data))
@@ -115,12 +115,7 @@ let escapeRegexPattern = (str: string): string => {
115
115
 
116
116
  // Generate file header
117
117
  let generateFileHeader = (~description: string): string =>
118
- `// ${description}
119
- // Generated by @f3liz/rescript-autogen-openapi
120
- // DO NOT EDIT - This file is auto-generated
121
-
122
- S.enableJson()
123
- `
118
+ Handlebars.render(Templates.fileHeader, {"description": description})
124
119
 
125
120
  // Indent code
126
121
  let indent = (code: string, level: int): string => {
@@ -131,25 +126,6 @@ let indent = (code: string, level: int): string => {
131
126
  ->Array.join("\n")
132
127
  }
133
128
 
134
- /**
135
- * Removes leading whitespace followed by a margin prefix from every line of a string.
136
- * This is useful for writing multiline templates in a more readable way.
137
- */
138
- let trimMargin = (text: string, ~marginPrefix="|") => {
139
- text
140
- ->String.split("\n")
141
- ->Array.map(line => {
142
- let trimmed = line->String.trimStart
143
- if trimmed->String.startsWith(marginPrefix) {
144
- trimmed->String.slice(~start=String.length(marginPrefix))
145
- } else {
146
- line
147
- }
148
- })
149
- ->Array.join("\n")
150
- ->String.trim
151
- }
152
-
153
129
  // ReScript keywords that need to be escaped
154
130
  let rescriptKeywords = [
155
131
  "and", "as", "assert", "async", "await", "catch", "class", "constraint",
@@ -166,12 +142,10 @@ let escapeKeyword = (name: string): string => rescriptKeywords->Array.includes(n
166
142
 
167
143
  // Generate documentation comment (single-line comments)
168
144
  let generateDocComment = (~summary=?, ~description=?, ()): string =>
169
- switch (summary, description) {
170
- | (None, None) => ""
171
- | (Some(s), None) => `// ${s}\n`
172
- | (None, Some(d)) => `// ${d}\n`
173
- | (Some(s), Some(d)) => `// ${s}\n// ${d}\n`
174
- }
145
+ Handlebars.render(
146
+ Templates.docComment,
147
+ {"summary": summary->Null.fromOption, "description": description->Null.fromOption},
148
+ )
175
149
 
176
150
  // Generate DocString comment (multi-line /** ... */ format) from markdown
177
151
  let generateDocString = (~summary=?, ~description=?, ()): string => {
@@ -186,8 +160,10 @@ let generateDocString = (~summary=?, ~description=?, ()): string => {
186
160
  let lines = text->String.trim->String.split("\n")->Array.map(String.trim)
187
161
  switch lines {
188
162
  | [] => ""
189
- | [line] => `/** ${line} */\n`
190
- | lines => "/**\n" ++ lines->Array.map(l => l == "" ? " *" : ` * ${l}`)->Array.join("\n") ++ "\n */\n"
163
+ | [line] =>
164
+ Handlebars.render(Templates.docCommentSingle, {"content": line})
165
+ | lines =>
166
+ Handlebars.render(Templates.docCommentMulti, {"lines": lines})
191
167
  }
192
168
  })->Option.getOr("")
193
169
  }
@@ -183,29 +183,15 @@ let generateOverrideMarkdown = (
183
183
  ]->Array.filterMap(x => x)
184
184
  )
185
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
186
+ Handlebars.render(Templates.overrideMarkdown, {
187
+ "metadataBlock": Array.join(metadata, "\n"),
188
+ "title": endpoint.summary->Option.getOr(endpoint.path),
189
+ "path": endpoint.path,
190
+ "methodUpper": endpoint.method->String.toUpperCase,
191
+ "operationName": operationName,
192
+ "defaultDesc": defaultDesc,
193
+ },
194
+ )
209
195
  }
210
196
 
211
197
  // Read override from file system
@@ -390,107 +376,7 @@ let generateOverrideReadme = (~host: option<string>=?, ~version: option<string>=
390
376
  let hostInfo = host->Option.getOr("Not specified")
391
377
  let versionInfo = version->Option.getOr("Not specified")
392
378
 
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
379
+ Handlebars.render(Templates.overrideReadme, {"hostInfo": hostInfo, "versionInfo": versionInfo})
494
380
  }
495
381
 
496
382
  // No automatic refresh - users should manually delete outdated files