@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,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// ThinWrapperGenerator.res - Generate ReScript thin wrappers with pipe-first ergonomics
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
let clientTypeCode = `
|
|
7
|
+
|type client = {
|
|
8
|
+
| baseUrl: string,
|
|
9
|
+
| token: option<string>,
|
|
10
|
+
| fetch: ${CodegenUtils.fetchTypeSignature},
|
|
11
|
+
|}
|
|
12
|
+
|`->CodegenUtils.trimMargin
|
|
13
|
+
|
|
14
|
+
let generateConnectFunction = (title) =>
|
|
15
|
+
`
|
|
16
|
+
|/** Create a client for ${title} */
|
|
17
|
+
|let connect = (~baseUrl: string, ~token: option<string>=?, ~fetch: ${CodegenUtils.fetchTypeSignature}, ()): client => {
|
|
18
|
+
| baseUrl,
|
|
19
|
+
| token,
|
|
20
|
+
| fetch,
|
|
21
|
+
|}
|
|
22
|
+
|`->CodegenUtils.trimMargin
|
|
23
|
+
|
|
24
|
+
let generateWrapperFunction = (~endpoint: endpoint, ~generatedModuleName: string) => {
|
|
25
|
+
let operationName = CodegenUtils.generateOperationName(
|
|
26
|
+
endpoint.operationId,
|
|
27
|
+
endpoint.path,
|
|
28
|
+
endpoint.method,
|
|
29
|
+
)
|
|
30
|
+
let hasRequestBody = endpoint.requestBody->Option.isSome
|
|
31
|
+
let docComment = endpoint.summary->Option.mapOr("", summary => {
|
|
32
|
+
let descriptionPart = endpoint.description->Option.mapOr("", description =>
|
|
33
|
+
description == summary ? "" : " - " ++ description
|
|
34
|
+
)
|
|
35
|
+
` /** ${summary}${descriptionPart} */\n`
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
let signature = hasRequestBody
|
|
39
|
+
? `let ${operationName} = (request: ${generatedModuleName}.${operationName}Request, ~client: client)`
|
|
40
|
+
: `let ${operationName} = (~client: client)`
|
|
41
|
+
|
|
42
|
+
let callArguments = hasRequestBody ? "~body=request, " : ""
|
|
43
|
+
|
|
44
|
+
`${docComment} ${signature}: promise<${generatedModuleName}.${operationName}Response> =>
|
|
45
|
+
${generatedModuleName}.${operationName}(${callArguments}~fetch=client.fetch)`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let generateWrapper = (
|
|
49
|
+
~spec: openAPISpec,
|
|
50
|
+
~endpoints,
|
|
51
|
+
~extensionEndpoints=[],
|
|
52
|
+
~outputDir,
|
|
53
|
+
~wrapperModuleName="Wrapper",
|
|
54
|
+
~generatedModulePrefix="",
|
|
55
|
+
~baseModulePrefix="",
|
|
56
|
+
) => {
|
|
57
|
+
let extensionOperationIds =
|
|
58
|
+
extensionEndpoints->Array.reduce(Dict.make(), (acc, endpoint) => {
|
|
59
|
+
let name = CodegenUtils.generateOperationName(
|
|
60
|
+
endpoint.operationId,
|
|
61
|
+
endpoint.path,
|
|
62
|
+
endpoint.method,
|
|
63
|
+
)
|
|
64
|
+
Dict.set(acc, name, true)
|
|
65
|
+
acc
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
let hasExtensions = Array.length(extensionEndpoints) > 0
|
|
69
|
+
|
|
70
|
+
let allEndpoints = Array.concat(
|
|
71
|
+
hasExtensions
|
|
72
|
+
? endpoints->Array.filter(endpoint => {
|
|
73
|
+
let name = CodegenUtils.generateOperationName(
|
|
74
|
+
endpoint.operationId,
|
|
75
|
+
endpoint.path,
|
|
76
|
+
endpoint.method,
|
|
77
|
+
)
|
|
78
|
+
!Dict.has(extensionOperationIds, name)
|
|
79
|
+
})
|
|
80
|
+
: endpoints,
|
|
81
|
+
extensionEndpoints,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
let endpointsByTag = OpenAPIParser.groupByTag(allEndpoints)
|
|
85
|
+
|
|
86
|
+
let modulesCode =
|
|
87
|
+
Dict.keysToArray(endpointsByTag)
|
|
88
|
+
->Array.map(tag => {
|
|
89
|
+
let moduleName = CodegenUtils.toPascalCase(tag)
|
|
90
|
+
let wrapperFunctions =
|
|
91
|
+
Dict.get(endpointsByTag, tag)
|
|
92
|
+
->Option.getOr([])
|
|
93
|
+
->Array.map(endpoint => {
|
|
94
|
+
let operationName = CodegenUtils.generateOperationName(
|
|
95
|
+
endpoint.operationId,
|
|
96
|
+
endpoint.path,
|
|
97
|
+
endpoint.method,
|
|
98
|
+
)
|
|
99
|
+
let isExtension = hasExtensions && Dict.has(extensionOperationIds, operationName)
|
|
100
|
+
let prefix =
|
|
101
|
+
(!isExtension && baseModulePrefix != "") ? baseModulePrefix : generatedModulePrefix
|
|
102
|
+
|
|
103
|
+
let targetModuleName = prefix != "" ? `${prefix}${moduleName}` : moduleName
|
|
104
|
+
generateWrapperFunction(~endpoint, ~generatedModuleName=targetModuleName)
|
|
105
|
+
})
|
|
106
|
+
->Array.join("\n\n")
|
|
107
|
+
|
|
108
|
+
`module ${moduleName} = {\n${wrapperFunctions}\n}`
|
|
109
|
+
})
|
|
110
|
+
->Array.join("\n\n")
|
|
111
|
+
|
|
112
|
+
let fileContent = `// Generated thin wrapper
|
|
113
|
+
|
|
114
|
+
${clientTypeCode}
|
|
115
|
+
|
|
116
|
+
${generateConnectFunction(spec.info.title)}
|
|
117
|
+
|
|
118
|
+
${modulesCode}`
|
|
119
|
+
|
|
120
|
+
Pipeline.fromFilesAndWarnings(
|
|
121
|
+
[{path: FileSystem.makePath(outputDir, `${wrapperModuleName}.res`), content: fileContent}],
|
|
122
|
+
[],
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// TypeScriptDtsGenerator.res - Generate TypeScript .d.ts definition files
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
let getFirstJsonSchema = (contentDict: dict<mediaType>): option<jsonSchema> =>
|
|
7
|
+
Dict.keysToArray(contentDict)
|
|
8
|
+
->Array.get(0)
|
|
9
|
+
->Option.flatMap(contentType => Dict.get(contentDict, contentType))
|
|
10
|
+
->Option.flatMap(mediaType => mediaType.schema)
|
|
11
|
+
|
|
12
|
+
let generateTypeScriptType = (name, description, schema) => {
|
|
13
|
+
let (irType, _) = SchemaIRParser.parseJsonSchema(schema)
|
|
14
|
+
IRToTypeScriptGenerator.generateNamedType(~namedSchema={name, description, type_: irType})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Generate TypeScript interface for request type
|
|
18
|
+
let generateRequestInterface = (~endpoint: endpoint, ~functionName) => {
|
|
19
|
+
let requestTypeName = `${CodegenUtils.toPascalCase(functionName)}Request`
|
|
20
|
+
endpoint.requestBody->Option.flatMap(body =>
|
|
21
|
+
getFirstJsonSchema(body.content)->Option.map(schema =>
|
|
22
|
+
generateTypeScriptType(requestTypeName, body.description, schema)
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Generate TypeScript interface for response type
|
|
28
|
+
let generateResponseInterface = (~endpoint: endpoint, ~functionName) => {
|
|
29
|
+
let responseTypeName = `${CodegenUtils.toPascalCase(functionName)}Response`
|
|
30
|
+
let successCodes = ["200", "201", "202", "204"]
|
|
31
|
+
let successResponse = successCodes
|
|
32
|
+
->Array.filterMap(code => Dict.get(endpoint.responses, code))
|
|
33
|
+
->Array.get(0)
|
|
34
|
+
|
|
35
|
+
successResponse
|
|
36
|
+
->Option.flatMap(response =>
|
|
37
|
+
response.content
|
|
38
|
+
->Option.flatMap(getFirstJsonSchema)
|
|
39
|
+
->Option.map(schema => generateTypeScriptType(responseTypeName, response.description->Some, schema))
|
|
40
|
+
)
|
|
41
|
+
->Option.getOr(`export type ${responseTypeName} = void;`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Generate method signature for endpoint in an interface
|
|
45
|
+
let generateMethodSignature = (~endpoint: endpoint, ~functionName) => {
|
|
46
|
+
let params = endpoint.requestBody->Option.isSome
|
|
47
|
+
? `client: MisskeyClient, request: ${CodegenUtils.toPascalCase(functionName)}Request`
|
|
48
|
+
: "client: MisskeyClient"
|
|
49
|
+
|
|
50
|
+
let docLines = endpoint.summary->Option.mapOr([], summary => {
|
|
51
|
+
let lines = [" /**", ` * ${summary}`]
|
|
52
|
+
endpoint.description->Option.forEach(description => {
|
|
53
|
+
if description != summary {
|
|
54
|
+
lines->Array.push(` * ${description}`)
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
lines->Array.push(" */")
|
|
58
|
+
lines
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
let code = `
|
|
62
|
+
|${docLines->Array.join("\n")}
|
|
63
|
+
| ${functionName}(${params}): Promise<${CodegenUtils.toPascalCase(functionName)}Response>;`
|
|
64
|
+
|
|
65
|
+
code->CodegenUtils.trimMargin
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Generate .d.ts file for a module (grouped by tag)
|
|
69
|
+
let generateModuleDts = (~moduleName, ~endpoints: array<endpoint>) => {
|
|
70
|
+
let interfaces =
|
|
71
|
+
endpoints
|
|
72
|
+
->Array.map(endpoint => {
|
|
73
|
+
let functionName = CodegenUtils.generateOperationName(
|
|
74
|
+
endpoint.operationId,
|
|
75
|
+
endpoint.path,
|
|
76
|
+
endpoint.method,
|
|
77
|
+
)
|
|
78
|
+
let requestPart =
|
|
79
|
+
generateRequestInterface(~endpoint, ~functionName)->Option.getOr("")
|
|
80
|
+
let responsePart = generateResponseInterface(~endpoint, ~functionName)
|
|
81
|
+
[requestPart, responsePart]->Array.filter(s => s != "")->Array.join("\n")
|
|
82
|
+
})
|
|
83
|
+
->Array.join("\n\n")
|
|
84
|
+
|
|
85
|
+
let methodSignatures =
|
|
86
|
+
endpoints
|
|
87
|
+
->Array.map(endpoint =>
|
|
88
|
+
generateMethodSignature(
|
|
89
|
+
~endpoint,
|
|
90
|
+
~functionName=CodegenUtils.generateOperationName(
|
|
91
|
+
endpoint.operationId,
|
|
92
|
+
endpoint.path,
|
|
93
|
+
endpoint.method,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
->Array.join("\n")
|
|
98
|
+
|
|
99
|
+
let header = `
|
|
100
|
+
|// TypeScript definitions for ${moduleName}
|
|
101
|
+
|// Generated by @f3liz/rescript-autogen-openapi
|
|
102
|
+
|// DO NOT EDIT
|
|
103
|
+
|
|
|
104
|
+
|import { MisskeyClient } from './index';
|
|
105
|
+
|import * as ComponentSchemas from './ComponentSchemas';
|
|
106
|
+
|`->CodegenUtils.trimMargin
|
|
107
|
+
|
|
108
|
+
`
|
|
109
|
+
|${header}
|
|
110
|
+
|
|
|
111
|
+
|${interfaces}
|
|
112
|
+
|
|
|
113
|
+
|export interface ${moduleName}Module {
|
|
114
|
+
|${methodSignatures}
|
|
115
|
+
|}
|
|
116
|
+
|
|
|
117
|
+
|export const ${moduleName}: ${moduleName}Module;
|
|
118
|
+
|`->CodegenUtils.trimMargin
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Generate ComponentSchemas.d.ts
|
|
122
|
+
let generateComponentSchemasDts = (~schemas: Dict.t<jsonSchema>) => {
|
|
123
|
+
let content =
|
|
124
|
+
Dict.toArray(schemas)
|
|
125
|
+
->Array.map(((name, schema)) => generateTypeScriptType(name, schema.description, schema))
|
|
126
|
+
->Array.join("\n\n")
|
|
127
|
+
|
|
128
|
+
`
|
|
129
|
+
|// TypeScript definitions for ComponentSchemas
|
|
130
|
+
|// Generated by @f3liz/rescript-autogen-openapi
|
|
131
|
+
|// DO NOT EDIT
|
|
132
|
+
|
|
|
133
|
+
|${content}
|
|
134
|
+
|`->CodegenUtils.trimMargin
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Generate main index.d.ts with MisskeyClient class
|
|
138
|
+
let generateIndexDts = (~moduleNames) => {
|
|
139
|
+
let imports = moduleNames->Array.map(m => `import { ${m}Module } from './${m}';`)->Array.join("\n")
|
|
140
|
+
let exports = moduleNames->Array.map(m => `export const ${m}: ${m}Module;`)->Array.join("\n")
|
|
141
|
+
|
|
142
|
+
`
|
|
143
|
+
|// TypeScript definitions
|
|
144
|
+
|// Generated by @f3liz/rescript-autogen-openapi
|
|
145
|
+
|// DO NOT EDIT
|
|
146
|
+
|
|
|
147
|
+
|${imports}
|
|
148
|
+
|
|
|
149
|
+
|export class MisskeyClient {
|
|
150
|
+
| constructor(baseUrl: string, token?: string);
|
|
151
|
+
| readonly baseUrl: string;
|
|
152
|
+
| readonly token?: string;
|
|
153
|
+
|}
|
|
154
|
+
|
|
|
155
|
+
|${exports}
|
|
156
|
+
|`->CodegenUtils.trimMargin
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Generate all .d.ts files for a spec
|
|
160
|
+
let generate = (~spec: openAPISpec, ~endpoints, ~outputDir): Pipeline.generationOutput => {
|
|
161
|
+
let endpointsByTag = OpenAPIParser.groupByTag(endpoints)
|
|
162
|
+
let moduleNames = []
|
|
163
|
+
let files =
|
|
164
|
+
Dict.toArray(endpointsByTag)
|
|
165
|
+
->Array.filterMap(((tag, tagEndpoints)) =>
|
|
166
|
+
if Array.length(tagEndpoints) > 0 {
|
|
167
|
+
let name = CodegenUtils.toPascalCase(tag)
|
|
168
|
+
moduleNames->Array.push(name)
|
|
169
|
+
Some({
|
|
170
|
+
FileSystem.path: FileSystem.makePath(outputDir, `types/${name}.d.ts`),
|
|
171
|
+
content: generateModuleDts(~moduleName=name, ~endpoints=tagEndpoints),
|
|
172
|
+
})
|
|
173
|
+
} else {
|
|
174
|
+
None
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
spec.components
|
|
179
|
+
->Option.flatMap(c => c.schemas)
|
|
180
|
+
->Option.forEach(schemas =>
|
|
181
|
+
files->Array.push({
|
|
182
|
+
path: FileSystem.makePath(outputDir, "types/ComponentSchemas.d.ts"),
|
|
183
|
+
content: generateComponentSchemasDts(~schemas=schemas),
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
files->Array.push({
|
|
188
|
+
path: FileSystem.makePath(outputDir, "types/index.d.ts"),
|
|
189
|
+
content: generateIndexDts(~moduleNames),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
{files, warnings: []}
|
|
193
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// TypeScriptWrapperGenerator.res - Generate TypeScript/JavaScript wrapper
|
|
4
|
+
open Types
|
|
5
|
+
|
|
6
|
+
let misskeyClientJsCode = `
|
|
7
|
+
|export class MisskeyClient {
|
|
8
|
+
| constructor(baseUrl, token) {
|
|
9
|
+
| this.baseUrl = baseUrl;
|
|
10
|
+
| this.token = token;
|
|
11
|
+
| }
|
|
12
|
+
|
|
|
13
|
+
| async _fetch(url, method, body) {
|
|
14
|
+
| const headers = { 'Content-Type': 'application/json' };
|
|
15
|
+
| if (this.token) {
|
|
16
|
+
| headers['Authorization'] = \`Bearer \${this.token}\`;
|
|
17
|
+
| }
|
|
18
|
+
| const response = await fetch(this.baseUrl + url, {
|
|
19
|
+
| method,
|
|
20
|
+
| headers,
|
|
21
|
+
| body: body ? JSON.stringify(body) : undefined,
|
|
22
|
+
| });
|
|
23
|
+
| return response.json();
|
|
24
|
+
| }
|
|
25
|
+
|}`->CodegenUtils.trimMargin
|
|
26
|
+
|
|
27
|
+
let generateWrapperMjs = (~endpoints, ~generatedModulePath) => {
|
|
28
|
+
let endpointsByTag = OpenAPIParser.groupByTag(endpoints)
|
|
29
|
+
let tags = Dict.keysToArray(endpointsByTag)
|
|
30
|
+
|
|
31
|
+
let imports =
|
|
32
|
+
tags
|
|
33
|
+
->Array.map(tag => {
|
|
34
|
+
let moduleName = CodegenUtils.toPascalCase(tag)
|
|
35
|
+
`import * as ${moduleName} from '${generatedModulePath}/${moduleName}.mjs';`
|
|
36
|
+
})
|
|
37
|
+
->Array.join("\n")
|
|
38
|
+
|
|
39
|
+
let wrappers =
|
|
40
|
+
tags
|
|
41
|
+
->Array.map(tag => {
|
|
42
|
+
let moduleName = CodegenUtils.toPascalCase(tag)
|
|
43
|
+
let methods =
|
|
44
|
+
Dict.get(endpointsByTag, tag)
|
|
45
|
+
->Option.getOr([])
|
|
46
|
+
->Array.map(endpoint => {
|
|
47
|
+
let functionName = CodegenUtils.generateOperationName(
|
|
48
|
+
endpoint.operationId,
|
|
49
|
+
endpoint.path,
|
|
50
|
+
endpoint.method,
|
|
51
|
+
)
|
|
52
|
+
let hasRequestBody = endpoint.requestBody->Option.isSome
|
|
53
|
+
let bodyArg = hasRequestBody ? "body: request, " : ""
|
|
54
|
+
`
|
|
55
|
+
| async ${functionName}(client${hasRequestBody ? ", request" : ""}) {
|
|
56
|
+
| return ${moduleName}.${functionName}({
|
|
57
|
+
| ${bodyArg}fetch: (url, method, body) => client._fetch(url, method, body)
|
|
58
|
+
| });
|
|
59
|
+
| },`
|
|
60
|
+
})
|
|
61
|
+
->Array.join("\n")
|
|
62
|
+
`
|
|
63
|
+
|export const ${moduleName} = {
|
|
64
|
+
|${methods}
|
|
65
|
+
|};`
|
|
66
|
+
})
|
|
67
|
+
->Array.join("\n\n")
|
|
68
|
+
|
|
69
|
+
`
|
|
70
|
+
|// Generated wrapper
|
|
71
|
+
|${imports}
|
|
72
|
+
|
|
|
73
|
+
|${misskeyClientJsCode}
|
|
74
|
+
|
|
|
75
|
+
|${wrappers}
|
|
76
|
+
|`->CodegenUtils.trimMargin
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let generateWrapperDts = (~endpoints) => {
|
|
80
|
+
let endpointsByTag = OpenAPIParser.groupByTag(endpoints)
|
|
81
|
+
let tags = Dict.keysToArray(endpointsByTag)
|
|
82
|
+
|
|
83
|
+
let imports =
|
|
84
|
+
tags
|
|
85
|
+
->Array.map(tag => {
|
|
86
|
+
let moduleName = CodegenUtils.toPascalCase(tag)
|
|
87
|
+
let typesToImport =
|
|
88
|
+
Dict.get(endpointsByTag, tag)
|
|
89
|
+
->Option.getOr([])
|
|
90
|
+
->Array.flatMap(endpoint => {
|
|
91
|
+
let pascalName = CodegenUtils.toPascalCase(
|
|
92
|
+
CodegenUtils.generateOperationName(endpoint.operationId, endpoint.path, endpoint.method),
|
|
93
|
+
)
|
|
94
|
+
if endpoint.requestBody->Option.isSome {
|
|
95
|
+
[` ${pascalName}Request,`, ` ${pascalName}Response,`]
|
|
96
|
+
} else {
|
|
97
|
+
[` ${pascalName}Response,`]
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
->Array.join("\n")
|
|
101
|
+
`import type {
|
|
102
|
+
${typesToImport}
|
|
103
|
+
} from '../types/${moduleName}.d.ts';`
|
|
104
|
+
})
|
|
105
|
+
->Array.join("\n")
|
|
106
|
+
|
|
107
|
+
let namespaces =
|
|
108
|
+
tags
|
|
109
|
+
->Array.map(tag => {
|
|
110
|
+
let moduleName = CodegenUtils.toPascalCase(tag)
|
|
111
|
+
let functions =
|
|
112
|
+
Dict.get(endpointsByTag, tag)
|
|
113
|
+
->Option.getOr([])
|
|
114
|
+
->Array.map(endpoint => {
|
|
115
|
+
let functionName = CodegenUtils.generateOperationName(
|
|
116
|
+
endpoint.operationId,
|
|
117
|
+
endpoint.path,
|
|
118
|
+
endpoint.method,
|
|
119
|
+
)
|
|
120
|
+
let pascalName = CodegenUtils.toPascalCase(functionName)
|
|
121
|
+
let docComment = endpoint.summary->Option.mapOr("", summary => {
|
|
122
|
+
let descriptionPart = endpoint.description->Option.mapOr("", description =>
|
|
123
|
+
description == summary ? "" : " - " ++ description
|
|
124
|
+
)
|
|
125
|
+
` /** ${summary}${descriptionPart} */\n`
|
|
126
|
+
})
|
|
127
|
+
let requestParam = endpoint.requestBody->Option.isSome ? `, request: ${pascalName}Request` : ""
|
|
128
|
+
`${docComment} export function ${functionName}(client: MisskeyClient${requestParam}): Promise<${pascalName}Response>;`
|
|
129
|
+
})
|
|
130
|
+
->Array.join("\n")
|
|
131
|
+
`
|
|
132
|
+
|export namespace ${moduleName} {
|
|
133
|
+
|${functions}
|
|
134
|
+
|}`
|
|
135
|
+
})
|
|
136
|
+
->Array.join("\n\n")
|
|
137
|
+
|
|
138
|
+
`
|
|
139
|
+
|// Generated TypeScript definitions for wrapper
|
|
140
|
+
|${imports}
|
|
141
|
+
|
|
|
142
|
+
|export class MisskeyClient {
|
|
143
|
+
| constructor(baseUrl: string, token?: string);
|
|
144
|
+
| readonly baseUrl: string;
|
|
145
|
+
| readonly token?: string;
|
|
146
|
+
|}
|
|
147
|
+
|
|
|
148
|
+
|${namespaces}
|
|
149
|
+
|`->CodegenUtils.trimMargin
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let generate = (~endpoints, ~outputDir, ~generatedModulePath="../generated") => {
|
|
153
|
+
Pipeline.fromFilesAndWarnings(
|
|
154
|
+
[
|
|
155
|
+
{
|
|
156
|
+
FileSystem.path: FileSystem.makePath(outputDir, "wrapper/index.mjs"),
|
|
157
|
+
content: generateWrapperMjs(~endpoints, ~generatedModulePath),
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
path: FileSystem.makePath(outputDir, "wrapper/index.d.ts"),
|
|
161
|
+
content: generateWrapperDts(~endpoints),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
[],
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// Error.res - Compact error and warning types with helpers
|
|
4
|
+
|
|
5
|
+
// Error context for debugging (defined here to avoid circular dependency)
|
|
6
|
+
type context = {
|
|
7
|
+
path: string,
|
|
8
|
+
operation: string,
|
|
9
|
+
schema: option<JSON.t>, // Use JSON.t to avoid circular dependency with Types.jsonSchema
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Structured error types (keep original names for backward compat)
|
|
13
|
+
type t =
|
|
14
|
+
| SpecResolutionError({url: string, message: string})
|
|
15
|
+
| SchemaParseError({context: context, reason: string})
|
|
16
|
+
| ReferenceError({ref: string, context: context})
|
|
17
|
+
| ValidationError({schema: string, input: JSON.t, issues: array<string>})
|
|
18
|
+
| CircularSchemaError({ref: string, depth: int, path: string})
|
|
19
|
+
| FileWriteError({filePath: string, message: string})
|
|
20
|
+
| InvalidConfigError({field: string, message: string})
|
|
21
|
+
| UnknownError({message: string, context: option<context>})
|
|
22
|
+
|
|
23
|
+
// Warning types
|
|
24
|
+
module Warning = {
|
|
25
|
+
type t =
|
|
26
|
+
| FallbackToJson({reason: string, context: context})
|
|
27
|
+
| UnsupportedFeature({feature: string, fallback: string, location: string})
|
|
28
|
+
| DepthLimitReached({depth: int, path: string})
|
|
29
|
+
| MissingSchema({ref: string, location: string})
|
|
30
|
+
| IntersectionNotFullySupported({location: string, note: string})
|
|
31
|
+
| ComplexUnionSimplified({location: string, types: string})
|
|
32
|
+
|
|
33
|
+
let toString = w =>
|
|
34
|
+
switch w {
|
|
35
|
+
| FallbackToJson({reason, context}) =>
|
|
36
|
+
`⚠️ Falling back to JSON.t at '${context.path}' (${context.operation}): ${reason}`
|
|
37
|
+
| UnsupportedFeature({feature, fallback, location}) =>
|
|
38
|
+
`⚠️ Unsupported feature '${feature}' at '${location}', using fallback: ${fallback}`
|
|
39
|
+
| DepthLimitReached({depth, path}) =>
|
|
40
|
+
`⚠️ Depth limit ${depth->Int.toString} reached at '${path}', using simplified type`
|
|
41
|
+
| MissingSchema({ref, location}) =>
|
|
42
|
+
`⚠️ Schema reference '${ref}' not found at '${location}'`
|
|
43
|
+
| IntersectionNotFullySupported({location, note}) =>
|
|
44
|
+
`⚠️ Intersection type at '${location}' not fully supported: ${note}`
|
|
45
|
+
| ComplexUnionSimplified({location, types}) =>
|
|
46
|
+
`⚠️ Complex union at '${location}' simplified (types: ${types})`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let print = warnings =>
|
|
50
|
+
if Array.length(warnings) > 0 {
|
|
51
|
+
Console.log("\n⚠️ Warnings:")
|
|
52
|
+
warnings->Array.forEach(w => Console.log(toString(w)))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Error helpers
|
|
57
|
+
let toString = e =>
|
|
58
|
+
switch e {
|
|
59
|
+
| SpecResolutionError({url, message}) => `Failed to resolve spec from '${url}': ${message}`
|
|
60
|
+
| SchemaParseError({context, reason}) =>
|
|
61
|
+
`Failed to parse schema at '${context.path}' (${context.operation}): ${reason}`
|
|
62
|
+
| ReferenceError({ref, context}) =>
|
|
63
|
+
`Failed to resolve reference '${ref}' at '${context.path}' (${context.operation})`
|
|
64
|
+
| ValidationError({schema, issues}) =>
|
|
65
|
+
`Validation failed for schema '${schema}': ${issues->Array.join(", ")}`
|
|
66
|
+
| CircularSchemaError({ref, depth, path}) =>
|
|
67
|
+
`Circular schema detected for '${ref}' at depth ${depth->Int.toString} (path: ${path})`
|
|
68
|
+
| FileWriteError({filePath, message}) => `Failed to write file '${filePath}': ${message}`
|
|
69
|
+
| InvalidConfigError({field, message}) => `Invalid configuration for field '${field}': ${message}`
|
|
70
|
+
| UnknownError({message, context}) =>
|
|
71
|
+
switch context {
|
|
72
|
+
| Some(ctx) => `Unknown error at '${ctx.path}' (${ctx.operation}): ${message}`
|
|
73
|
+
| None => `Unknown error: ${message}`
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create context helper
|
|
78
|
+
let makeContext = (~path, ~operation, ~schema=?, ()) => {
|
|
79
|
+
path,
|
|
80
|
+
operation,
|
|
81
|
+
schema,
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// Config.res - Generation configuration types
|
|
4
|
+
|
|
5
|
+
type generationStrategy =
|
|
6
|
+
| Separate
|
|
7
|
+
| SharedBase
|
|
8
|
+
|
|
9
|
+
type breakingChangeHandling =
|
|
10
|
+
| Error
|
|
11
|
+
| Warn
|
|
12
|
+
| Ignore
|
|
13
|
+
|
|
14
|
+
type forkSpecConfig = {
|
|
15
|
+
name: string,
|
|
16
|
+
specPath: string,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type generationTargets = {
|
|
20
|
+
rescriptApi: bool, // Generate base ReScript API (always true by default)
|
|
21
|
+
rescriptWrapper: bool, // Generate ReScript thin wrapper (pipe-first)
|
|
22
|
+
typescriptDts: bool, // Generate .d.ts type definitions
|
|
23
|
+
typescriptWrapper: bool, // Generate TypeScript/JavaScript wrapper
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type t = {
|
|
27
|
+
specPath: string,
|
|
28
|
+
forkSpecs: option<array<forkSpecConfig>>,
|
|
29
|
+
outputDir: string,
|
|
30
|
+
strategy: generationStrategy,
|
|
31
|
+
modulePerTag: bool,
|
|
32
|
+
includeTags: option<array<string>>,
|
|
33
|
+
excludeTags: option<array<string>>,
|
|
34
|
+
generateDiffReport: bool,
|
|
35
|
+
breakingChangeHandling: breakingChangeHandling,
|
|
36
|
+
generateDocOverrides: option<bool>,
|
|
37
|
+
docOverrideDir: option<string>,
|
|
38
|
+
targets: option<generationTargets>, // Generation targets (what to generate)
|
|
39
|
+
dtsOutputDir: option<string>, // Output directory for .d.ts files (default: "types")
|
|
40
|
+
wrapperOutputDir: option<string>, // Output directory for wrapper files (default: "wrapper")
|
|
41
|
+
baseInstanceName: option<string>, // Subdirectory name for base instance (e.g., "misskey-io")
|
|
42
|
+
baseModulePrefix: option<string>, // Module prefix for base instance (e.g., "MisskeyIo")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Default configuration
|
|
46
|
+
let make = (
|
|
47
|
+
~specPath,
|
|
48
|
+
~outputDir,
|
|
49
|
+
~strategy=SharedBase,
|
|
50
|
+
~modulePerTag=true,
|
|
51
|
+
~includeTags=?,
|
|
52
|
+
~excludeTags=?,
|
|
53
|
+
~generateDiffReport=true,
|
|
54
|
+
~breakingChangeHandling=Warn,
|
|
55
|
+
~forkSpecs=?,
|
|
56
|
+
~generateDocOverrides=?,
|
|
57
|
+
~docOverrideDir=?,
|
|
58
|
+
~targets=?,
|
|
59
|
+
~dtsOutputDir=?,
|
|
60
|
+
~wrapperOutputDir=?,
|
|
61
|
+
~baseInstanceName=?,
|
|
62
|
+
~baseModulePrefix=?,
|
|
63
|
+
(),
|
|
64
|
+
) => {
|
|
65
|
+
specPath,
|
|
66
|
+
outputDir,
|
|
67
|
+
strategy,
|
|
68
|
+
modulePerTag,
|
|
69
|
+
includeTags,
|
|
70
|
+
excludeTags,
|
|
71
|
+
generateDiffReport,
|
|
72
|
+
breakingChangeHandling,
|
|
73
|
+
forkSpecs,
|
|
74
|
+
generateDocOverrides,
|
|
75
|
+
docOverrideDir,
|
|
76
|
+
targets,
|
|
77
|
+
dtsOutputDir,
|
|
78
|
+
wrapperOutputDir,
|
|
79
|
+
baseInstanceName,
|
|
80
|
+
baseModulePrefix,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Default generation targets
|
|
84
|
+
let defaultTargets = (): generationTargets => {
|
|
85
|
+
rescriptApi: true,
|
|
86
|
+
rescriptWrapper: false,
|
|
87
|
+
typescriptDts: false,
|
|
88
|
+
typescriptWrapper: false,
|
|
89
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
// GenerationContext.res - Shared context type for IR code generators
|
|
4
|
+
|
|
5
|
+
type t = {
|
|
6
|
+
mutable warnings: array<CodegenError.Warning.t>,
|
|
7
|
+
path: string,
|
|
8
|
+
insideComponentSchemas: bool, // Whether we're generating inside ComponentSchemas module
|
|
9
|
+
availableSchemas: option<array<string>>, // Schemas available in current module (for fork schemas)
|
|
10
|
+
modulePrefix: string, // Module prefix for qualified references (e.g., "MisskeyIo")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let make = (~path, ~insideComponentSchemas=false, ~availableSchemas=?, ~modulePrefix="", ()): t => {
|
|
14
|
+
warnings: [],
|
|
15
|
+
path,
|
|
16
|
+
insideComponentSchemas,
|
|
17
|
+
availableSchemas,
|
|
18
|
+
modulePrefix,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let addWarning = (ctx: t, warning: CodegenError.Warning.t): unit => {
|
|
22
|
+
ctx.warnings->Array.push(warning)
|
|
23
|
+
}
|