@hasagi/schema 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/package.json +4 -4
- package/generate-openapi-v3.d.ts +0 -3
- package/generate-openapi-v3.js +0 -243
- package/open-api-types.d.ts +0 -944
- package/openapi-schema.d.ts +0 -14
- package/openapi-schema.js +0 -134
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @hasagi/schema
|
|
2
|
+
|
|
3
|
+
The schema / type-generation toolkit behind Hasagi. It reads the League of Legends client's own API
|
|
4
|
+
description (the LCU "help" schema, exposed by a running client), builds an OpenAPI v3 (swagger)
|
|
5
|
+
document from it, and generates the TypeScript definitions that power typed LCU requests and events.
|
|
6
|
+
|
|
7
|
+
This is the engine used by the [`@hasagi/cli`](https://www.npmjs.com/package/@hasagi/cli) `schema`
|
|
8
|
+
command and the generated types shipped in
|
|
9
|
+
[`@hasagi/core/types`](https://www.npmjs.com/package/@hasagi/core) and
|
|
10
|
+
[`@hasagi/types`](https://www.npmjs.com/package/@hasagi/types).
|
|
11
|
+
|
|
12
|
+
> Most users don't need this package directly — use `hasagi schema` (from `@hasagi/cli`) to generate
|
|
13
|
+
> types, or just consume the pre-generated types. Use `@hasagi/schema` only if you're building your
|
|
14
|
+
> own generation pipeline.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @hasagi/schema
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { getExtendedHelp, getSwagger, getTypeScript } from "@hasagi/schema";
|
|
26
|
+
|
|
27
|
+
// 1. Fetch and normalize the LCU help schema (requires a running League client).
|
|
28
|
+
const xhelp = await getExtendedHelp();
|
|
29
|
+
|
|
30
|
+
// 2. Build an OpenAPI v3 (swagger) document.
|
|
31
|
+
const swagger = await getSwagger(xhelp);
|
|
32
|
+
|
|
33
|
+
// 3. Generate the TypeScript definitions.
|
|
34
|
+
const { lcuTypes, lcuEndpoints, lcuEvents } = await getTypeScript(swagger, xhelp /*, namespace? */);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **`getExtendedHelp()`** — retrieves and normalizes the LCU's API description from a running client.
|
|
38
|
+
- **`getSwagger(xhelp)`** — produces an OpenAPI v3 schema object (with helpers for generating types).
|
|
39
|
+
- **`getTypeScript(swagger, xhelp, namespace?)`** — returns the generated `lcuTypes`, `lcuEndpoints`
|
|
40
|
+
and `lcuEvents` source strings. Pass a `namespace` to wrap the generated types.
|
|
41
|
+
|
|
42
|
+
## Disclaimer
|
|
43
|
+
|
|
44
|
+
Hasagi is not endorsed by Riot Games and does not reflect the views or opinions of Riot Games or
|
|
45
|
+
anyone officially involved in producing or managing Riot Games properties. Riot Games and all
|
|
46
|
+
associated properties are trademarks or registered trademarks of Riot Games, Inc.
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasagi/schema",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"hasagi"
|
|
6
6
|
],
|
|
7
7
|
"author": "dysolix",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@hasagi/core": "^0.
|
|
11
|
-
"axios": "^1.
|
|
10
|
+
"@hasagi/core": "^0.7.0",
|
|
11
|
+
"axios": "^1.17.0"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js"
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
"type": "module",
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
20
|
-
"url": "https://github.com/dysolix/hasagi-schema.git"
|
|
20
|
+
"url": "git+https://github.com/dysolix/hasagi-schema.git"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/generate-openapi-v3.d.ts
DELETED
package/generate-openapi-v3.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import HasagiLiteClient from "@hasagi/core";
|
|
2
|
-
function getRootType(input) {
|
|
3
|
-
const isObject = input.fields.length > 0;
|
|
4
|
-
const isEnum = input.values.length > 0;
|
|
5
|
-
const required = [];
|
|
6
|
-
const properties = {};
|
|
7
|
-
input.fields.forEach(f => {
|
|
8
|
-
if (properties[f.name] !== undefined) {
|
|
9
|
-
console.log(`Duplicate field '${f.name}' in type '${input.name}'`);
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
properties[f.name] = getType(f);
|
|
13
|
-
if (!f.optional)
|
|
14
|
-
required.push(f.name);
|
|
15
|
-
});
|
|
16
|
-
return {
|
|
17
|
-
type: isEnum ? "string" : "object",
|
|
18
|
-
description: input.description,
|
|
19
|
-
properties: isObject ? properties : undefined,
|
|
20
|
-
enum: isEnum ? input.values.sort((v1, v2) => v2.value - v1.value).map(v => v.name) : undefined,
|
|
21
|
-
additionalProperties: !isObject && !isEnum,
|
|
22
|
-
required: required.length > 0 ? required : undefined
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function getType(input) {
|
|
26
|
-
const type = typeof input === "string" ? input : input.type.type;
|
|
27
|
-
switch (type) {
|
|
28
|
-
case "string":
|
|
29
|
-
return { type: "string" };
|
|
30
|
-
case "uint8":
|
|
31
|
-
case "uint16":
|
|
32
|
-
case "uint32":
|
|
33
|
-
case "uint64":
|
|
34
|
-
return { type: "integer", format: type, minimum: 0 };
|
|
35
|
-
case "int8":
|
|
36
|
-
case "int16":
|
|
37
|
-
case "int32":
|
|
38
|
-
case "int64":
|
|
39
|
-
return { type: "integer", format: type };
|
|
40
|
-
case "bool":
|
|
41
|
-
return { type: "boolean" };
|
|
42
|
-
case "double":
|
|
43
|
-
case "float":
|
|
44
|
-
return { type: "number", format: type };
|
|
45
|
-
case "vector":
|
|
46
|
-
return { type: "array", items: getType(input.type.elementType) };
|
|
47
|
-
case "map": {
|
|
48
|
-
return { type: "object", additionalProperties: getType(input.type.elementType) };
|
|
49
|
-
}
|
|
50
|
-
case "object":
|
|
51
|
-
return { type: "object", additionalProperties: true };
|
|
52
|
-
case "":
|
|
53
|
-
return undefined;
|
|
54
|
-
default:
|
|
55
|
-
return { $ref: `#/components/schemas/${type}` };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export async function generateOpenAPIv3(schema) {
|
|
59
|
-
const client = new HasagiLiteClient();
|
|
60
|
-
await client.connect();
|
|
61
|
-
const region = await client.request({ method: "get", url: "/riotclient/region-locale" });
|
|
62
|
-
const { version } = await client.request({ method: "get", url: "/system/v1/builds" });
|
|
63
|
-
const functionsWithMissingData = [];
|
|
64
|
-
const openAPISchema = {
|
|
65
|
-
openapi: "3.0.0",
|
|
66
|
-
info: {
|
|
67
|
-
title: "LCU SCHEMA",
|
|
68
|
-
description: "",
|
|
69
|
-
version
|
|
70
|
-
},
|
|
71
|
-
components: { schemas: {} },
|
|
72
|
-
paths: {}
|
|
73
|
-
};
|
|
74
|
-
let tags = [];
|
|
75
|
-
schema.types.forEach(type => openAPISchema.components.schemas[type.name] = getRootType(type));
|
|
76
|
-
schema.functions.forEach(func => {
|
|
77
|
-
if (func.overridden) {
|
|
78
|
-
functionsWithMissingData.push(func.name);
|
|
79
|
-
}
|
|
80
|
-
if (func.method === null || func.path === null) {
|
|
81
|
-
console.log(`Function '${func.name}' does not have a http method or path.`);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
openAPISchema.paths[func.path] = openAPISchema.paths[func.path] ?? {};
|
|
85
|
-
const operation = endpointToOperation(func, openAPISchema);
|
|
86
|
-
// @ts-expect-error
|
|
87
|
-
openAPISchema.paths[func.path][func.method.toLowerCase()] = operation;
|
|
88
|
-
});
|
|
89
|
-
let tagCount = {};
|
|
90
|
-
Object.entries(openAPISchema.paths).forEach(([path, methods]) => {
|
|
91
|
-
Object.values(methods).forEach((operation) => {
|
|
92
|
-
operation.tags?.forEach(tag => tagCount[tag] = (tagCount[tag] ?? 0) + 1);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
Object.entries(openAPISchema.paths).forEach(([path, methods]) => {
|
|
96
|
-
Object.values(methods).forEach((operation) => {
|
|
97
|
-
operation.tags = operation.tags?.filter(tag => tagCount[tag] > 1 || tag.startsWith("Plugin")) ?? [];
|
|
98
|
-
if ((operation.tags?.length ?? 0) < 1) {
|
|
99
|
-
operation.tags = [path.split("/")[1]];
|
|
100
|
-
}
|
|
101
|
-
if (operation.tags.length === 0)
|
|
102
|
-
operation.tags.push("other");
|
|
103
|
-
operation.tags = operation.tags.map(tag => tag.startsWith("Plugin") ? tag : tag.toLowerCase());
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
tagCount = {};
|
|
107
|
-
Object.entries(openAPISchema.paths).forEach(([path, methods]) => {
|
|
108
|
-
Object.values(methods).forEach((operation) => {
|
|
109
|
-
operation.tags?.forEach(tag => tagCount[tag] = (tagCount[tag] ?? 0) + 1);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
Object.entries(openAPISchema.paths).forEach(([path, methods]) => {
|
|
113
|
-
Object.values(methods).forEach((operation) => {
|
|
114
|
-
operation.tags = operation.tags?.filter(tag => tagCount[tag] > 1 || tag.startsWith("Plugin")) ?? [];
|
|
115
|
-
if (operation.tags.length === 0)
|
|
116
|
-
operation.tags.push("other");
|
|
117
|
-
operation.tags = operation.tags
|
|
118
|
-
.map(tag => tag.startsWith("Plugin") ? tag : tag.toLowerCase())
|
|
119
|
-
.map(tag => tag.replace("$", ""));
|
|
120
|
-
tags.push(...operation.tags.filter(tag => !tags.includes(tag)));
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
openAPISchema.tags = tags.map(tag => {
|
|
124
|
-
return {
|
|
125
|
-
name: tag
|
|
126
|
-
};
|
|
127
|
-
}).sort((t1, t2) => {
|
|
128
|
-
if (t1.name.includes("Plugin") && !t2.name.includes("Plugin"))
|
|
129
|
-
return 1;
|
|
130
|
-
if (!t1.name.includes("Plugin") && t2.name.includes("Plugin"))
|
|
131
|
-
return -1;
|
|
132
|
-
if (t1.name.includes("Plugin") && t2.name.includes("Plugin")) {
|
|
133
|
-
if (t1.name.includes("Plugin lol") && !t2.name.includes("Plugin lol"))
|
|
134
|
-
return 1;
|
|
135
|
-
if (!t1.name.includes("Plugin lol") && t2.name.includes("Plugin lol"))
|
|
136
|
-
return -1;
|
|
137
|
-
}
|
|
138
|
-
if (t1.name == "other" && t2.name != "other")
|
|
139
|
-
return 1;
|
|
140
|
-
if (t1.name != "other" && t2.name == "other")
|
|
141
|
-
return -1;
|
|
142
|
-
return t1.name.localeCompare(t2.name);
|
|
143
|
-
});
|
|
144
|
-
let i = 1;
|
|
145
|
-
openAPISchema.info.description =
|
|
146
|
-
`
|
|
147
|
-
Auto-generated using LCU's /help endpoint.
|
|
148
|
-
The following endpoints are not entirely auto-generated because their /help response is missing necessary fields:
|
|
149
|
-
|
|
150
|
-
${functionsWithMissingData.map(func => `${i++}.\t${func}`).join("\n")}
|
|
151
|
-
|
|
152
|
-
### Disclaimer
|
|
153
|
-
|
|
154
|
-
dysolix.dev is not endorsed by Riot Games and does not reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties.
|
|
155
|
-
Riot Games and all associated properties are trademarks or registered trademarks of Riot Games, Inc`;
|
|
156
|
-
return openAPISchema;
|
|
157
|
-
}
|
|
158
|
-
function endpointToOperation(endpoint, schema) {
|
|
159
|
-
let parameters = undefined;
|
|
160
|
-
let response = { description: "Success response" };
|
|
161
|
-
let requestBody = undefined;
|
|
162
|
-
parameters = endpoint.pathParams?.map(param => {
|
|
163
|
-
const p = endpoint.arguments.find(arg => arg.name.replace("+", "") === param.replace("+", ""));
|
|
164
|
-
return {
|
|
165
|
-
in: "path",
|
|
166
|
-
name: param,
|
|
167
|
-
required: true,
|
|
168
|
-
schema: p === undefined ? { type: "string" } : getType(p)
|
|
169
|
-
};
|
|
170
|
-
}) ?? [];
|
|
171
|
-
const nonPathParam = endpoint.arguments.slice(endpoint.pathParams?.length ?? 0);
|
|
172
|
-
if (nonPathParam.length > 1) {
|
|
173
|
-
//console.log(`Endpoint '${endpoint.name}' has more than one non-path parameter.`)
|
|
174
|
-
nonPathParam.forEach(arg => {
|
|
175
|
-
parameters.push({
|
|
176
|
-
in: "query",
|
|
177
|
-
name: arg.name,
|
|
178
|
-
schema: getType(arg),
|
|
179
|
-
required: !arg.optional
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
else if (endpoint.method === "GET" || endpoint.method === "DELETE" || endpoint.method === "HEAD" || endpoint.method === "OPTIONS" || endpoint.method === "TRACE") {
|
|
184
|
-
const params = endpoint.arguments.slice(parameters.length).flatMap(arg => {
|
|
185
|
-
const type = getType(arg);
|
|
186
|
-
if ("$ref" in type) {
|
|
187
|
-
const schemaObject = schema.components.schemas[type.$ref.split("/").at(-1)];
|
|
188
|
-
if (schemaObject.properties)
|
|
189
|
-
return Object.entries(schemaObject.properties).map(entry => {
|
|
190
|
-
return {
|
|
191
|
-
in: "query",
|
|
192
|
-
name: entry[0],
|
|
193
|
-
type: entry[1],
|
|
194
|
-
};
|
|
195
|
-
});
|
|
196
|
-
return {
|
|
197
|
-
in: "query",
|
|
198
|
-
schema: type,
|
|
199
|
-
name: arg.name
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
else {
|
|
203
|
-
return {
|
|
204
|
-
in: "query",
|
|
205
|
-
required: !arg.optional,
|
|
206
|
-
name: arg.name,
|
|
207
|
-
schema: getType(arg)
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
parameters.push(...params);
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
const bodyType = endpoint.arguments.slice(parameters.length).at(0);
|
|
215
|
-
if (bodyType)
|
|
216
|
-
requestBody = {
|
|
217
|
-
content: {
|
|
218
|
-
"application/json": {
|
|
219
|
-
schema: getType(bodyType)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
const returnType = getType({ type: endpoint.returns });
|
|
225
|
-
response = returnType !== undefined ? { content: { "application/json": { schema: returnType } }, description: "Success response" } : { description: "Success response" };
|
|
226
|
-
const tags = [];
|
|
227
|
-
if (endpoint.path?.startsWith("/lol-"))
|
|
228
|
-
tags.push("Plugin " + endpoint.path.split("/")[1]);
|
|
229
|
-
else if (endpoint.path?.startsWith("/{plugin}"))
|
|
230
|
-
tags.push("Plugin Asset Serving");
|
|
231
|
-
else
|
|
232
|
-
tags.push(endpoint.path.split("/")[1]);
|
|
233
|
-
return {
|
|
234
|
-
operationId: endpoint.name,
|
|
235
|
-
description: endpoint.description,
|
|
236
|
-
tags: endpoint.tags.filter(tag => !["Plugins", "$remoting-binding-module"].includes(tag)),
|
|
237
|
-
parameters,
|
|
238
|
-
requestBody,
|
|
239
|
-
responses: {
|
|
240
|
-
"2XX": response
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|