@compassdigital/sdk.typescript 3.14.0 → 3.15.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/gen.ts DELETED
@@ -1,442 +0,0 @@
1
-
2
- import fs from "fs";
3
- import {
4
- analyse,
5
- load,
6
- Schema,
7
- Printer,
8
- DocumentDetails,
9
- OperationDetails,
10
- isMethod,
11
- } from "@icholy/openapi-ts";
12
- import prettier from "prettier";
13
- import path from "path";
14
- import ejs from "ejs";
15
- import yargs from "yargs/yargs";
16
- import { hideBin } from "yargs/helpers";
17
-
18
- interface Manifest {
19
- services: ManifestService[];
20
- }
21
-
22
- interface ManifestService {
23
- name: string; // service name
24
- swagger: string; // swagger url or file
25
- }
26
-
27
- interface ClientImport {
28
- path: string;
29
- name: string;
30
- }
31
-
32
- interface MethodParam {
33
- name: string;
34
- type: string;
35
- required: boolean;
36
- description: string;
37
- }
38
-
39
- interface ClientMethod {
40
- comment: string;
41
- name: string;
42
- params: MethodParam[];
43
- args: string[];
44
- response: string;
45
- }
46
-
47
- /**
48
- * The method and path combined into snake and pascal style names.
49
- */
50
- interface OperationName {
51
- snake: string; // used for method names
52
- pascal: string; // used for type names
53
- }
54
-
55
- /**
56
- * This is a mapping between "${method} ${path}" and the default
57
- * operationId.
58
- */
59
- const operationIDs : Record<string, string> = {
60
- "get /location/group/{id}/deliverydestination": "get_location_group_deliverydestinations",
61
- "get /location/search": "get_location_search",
62
- "get /promo": "get_promos",
63
- "get /schedule": "get_schedules",
64
- "get /brand": "get_brands",
65
- "get /announcement/resource": "get_resources",
66
- "get /order/location/brand/{id}": "get_order_location_brand",
67
- };
68
-
69
- export class CodeGenerator {
70
-
71
- private imports: ClientImport[] = [];
72
- private methods: ClientMethod[] = [];
73
-
74
- async manifest(manifest: Manifest): Promise<void> {
75
- // generate service types
76
- await fs.promises.mkdir(path.join("src", "interface"), { recursive: true });
77
- for (const service of manifest.services) {
78
- console.log(`reading: ${service.swagger}`);
79
- const code = await this.service(service, false);
80
- const filename = path.join("src", "interface", `${service.name}.ts`);
81
- await fs.promises.writeFile(filename, code);
82
- }
83
- // generate client
84
- const code = await this.client();
85
- const filename = path.join("src", "index.ts");
86
- await fs.promises.writeFile(filename, code);
87
- }
88
-
89
- async client(): Promise<string> {
90
- const print = new Printer();
91
- // "do not modify" warning
92
- print.comment("THIS FILE IS AUTOMATICALLY GENERATED, DO NOT MODIFY");
93
- print.blank();
94
- // client imports
95
- const imports: Record<string, string[]> = {};
96
- for (const _import of this.imports) {
97
- imports[_import.path] ??= [];
98
- imports[_import.path].push(_import.name);
99
- }
100
- for (const [path, names] of Object.entries(imports)) {
101
- print.import(path, names);
102
- print.blank();
103
- }
104
- // client methods
105
- const template = fs.readFileSync("template.ejs", "utf-8");
106
- print.raw(ejs.render(template, { methods: this.methods }));
107
- return prettier.format(print.code(), { parser: "typescript", printWidth: 100 });
108
- }
109
-
110
- async service(service: ManifestService, ambient: boolean): Promise<string> {
111
- const print = new Printer();
112
- const doc = await load(service.swagger);
113
- const details = analyse(doc);
114
- this.transform(details, print, service, ambient);
115
- return prettier.format(print.code(), { parser: "typescript", printWidth: 100 });
116
- }
117
-
118
- /**
119
- * Add an import that the client will need.
120
- */
121
- private addImport(service: ManifestService, name: string): void {
122
- this.imports.push({
123
- name,
124
- path: `./interface/${service.name}`,
125
- });
126
- }
127
-
128
- /**
129
- * Generate typescript code from the document details.
130
- */
131
- private transform(doc: DocumentDetails, print: Printer, service: ManifestService, ambient: boolean): void {
132
- // "do not modify" warning
133
- print.comment("THIS FILE IS AUTOMATICALLY GENERATED, DO NOT MODIFY");
134
- print.blank();
135
- // declaration if it's ambient
136
- if (ambient) {
137
- print.raw(`declare module "@compassdigital/sdk.typescript/interface/${service.name}" {`);
138
- }
139
- // utility types
140
- if (ambient) {
141
- print.raw("export type RequestQuery<T extends object> = { [K in keyof T]: T[K] extends number|undefined ? string : T[K]; }");
142
- } else {
143
- print.import("./util", ["RequestQuery"]);
144
- }
145
- print.blank();
146
- // definitions
147
- for (const [name, schema] of Object.entries(doc.definitions)) {
148
- // allow additional propties in meta objects.
149
- const { meta } = schema.properties;
150
- if (meta?.type === "object" && !meta.additional) {
151
- meta.additional = new Schema();
152
- }
153
- print.schema(schema, name);
154
- print.blank();
155
- }
156
- // routes
157
- for (const op of doc.operations) {
158
- const { params, path } = op;
159
- for (const skipped of params.skipped) {
160
- console.warn("SKIPPED", skipped);
161
- }
162
- // add missing path parameters
163
- for (const name of this.findPathParams(path)) {
164
- if (!params.path.properties[name]) {
165
- params.path.setProperty(name, new Schema("string", {
166
- required: true,
167
- description: "TODO: add parameter to swagger.json",
168
- }));
169
- }
170
- }
171
- // make all path parameters required and valid path types.
172
- for (const param of Object.values(params.path.properties)) {
173
- if (!param.required) {
174
- param.required = true;
175
- if (param.description) {
176
- param.description += "; ";
177
- }
178
- param.description += "TODO: mark parameter as required in swagger";
179
- }
180
- if (param.isRef() || param.type === "object") {
181
- if (param.description) {
182
- param.description += "; ";
183
- }
184
- param.description += `TODO: cannot use ${param.type} as path parameter`;
185
- param.type = "string";
186
- }
187
- }
188
- // add nocache query parameter if x-cache-control is specified
189
- if (this.hasCacheControl(op) && !params.query.properties["nocache"]) {
190
- params.query.setProperty("nocache", new Schema("boolean"));
191
- }
192
- const name = this.inferName(op, service);
193
- let comment = `${op.method.toUpperCase()} ${path}`;
194
- if (op.obj.summary) {
195
- comment += ` - ${op.obj.summary}`;
196
- }
197
-
198
- print.comment(comment);
199
- print.blank();
200
- // path parameters
201
- if (!params.path.isEmpty()) {
202
- const pathT = `${name.pascal}Path`;
203
- print.schema(params.path, pathT);
204
- print.blank();
205
- }
206
- // query parameters
207
- const queryT = `${name.pascal}Query`;
208
- if (!params.query.isEmpty()) {
209
- this.addImport(service, queryT);
210
- print.schema(params.query, queryT);
211
- print.blank();
212
- }
213
- // body
214
- const bodyT = `${name.pascal}Body`;
215
- if (!params.body.isEmpty()) {
216
- this.addImport(service, bodyT);
217
- print.schema(params.body, bodyT);
218
- print.blank();
219
- }
220
- // response
221
- const responseT = `${name.pascal}Response`;
222
- this.addImport(service, responseT);
223
- print.schema(params.response, responseT);
224
- print.blank();
225
- // server request
226
- const requestT = `${name.pascal}Request`;
227
- print.schema(this.toRequestSchema(name, op), requestT);
228
- print.blank();
229
-
230
- // method parameters
231
- const method: ClientMethod = {
232
- comment: comment,
233
- name: name.snake,
234
- params:[],
235
- args: [`"${service.name}"`, `"${name.snake}"`, `"${op.method}"`],
236
- response: responseT,
237
- }
238
- // turn the path into a template string and add the
239
- // corresponding method parameters.
240
- let pathexpr = "`" + path + "`";
241
- for (const name of this.findPathParams(path)) {
242
- const schema = params.path.properties[name];
243
- method.params.push({
244
- name: name,
245
- type: Printer.type(schema),
246
- required: true,
247
- description: schema.description,
248
- });
249
- pathexpr = pathexpr.replace(`{${name}}`, "${" + name + "}");
250
- }
251
- method.args.push(pathexpr);
252
- // if there's a body, make that a parameter too.
253
- if (!params.body.isEmpty()) {
254
- method.params.push({
255
- name: "body",
256
- type: bodyT,
257
- required: true,
258
- description: params.body.description,
259
- });
260
- method.args.push("body");
261
- } else {
262
- method.args.push("null");
263
- }
264
- // build the options type.
265
- const options = new Schema("empty");
266
- if (!params.query.isEmpty()) {
267
- const query = new Schema(queryT);
268
- if (params.query.hasRequired()) {
269
- query.required = true;
270
- options.required = true;
271
- }
272
- options.setProperty("query", query);
273
- }
274
- options.merge(new Schema("RequestOptions"));
275
- method.params.push({
276
- name: "options",
277
- type: Printer.type(options),
278
- required: options.required,
279
- description: "additional request options",
280
- });
281
- method.args.push("options");
282
- this.methods.push(method);
283
- }
284
- // close declaration
285
- if (ambient) {
286
- print.raw(`}`);
287
- }
288
- }
289
-
290
- /**
291
- * Infer the operation name from the method and parameter.
292
- * These are a bunch of dirty hacks to make the generated names consistent.
293
- */
294
- inferName(op: OperationDetails, service: ManifestService): OperationName {
295
- let { method, path } = op;
296
- let path_segments: string[] = [];
297
- // use the operationId from our collection if we have one.
298
- // TODO: update the swagger.json definitions so we don't need to do this.
299
- let operationID: string | undefined = operationIDs[`${op.method} ${op.path}`];
300
- if (!operationID) {
301
- operationID = op.obj.operationId;
302
- }
303
- if (operationID) {
304
- const parts = operationID.split("_");
305
- if (parts.length > 1) {
306
- // replace common operationId values with the canonical ones.
307
- if (parts[0] === "create") {
308
- parts[0] = "post";
309
- }
310
- if (parts[0] === "update") {
311
- parts[0] = "put";
312
- }
313
- if (parts[0] === "remove") {
314
- parts[0] = "delete";
315
- }
316
- if (parts[0] === "find") {
317
- parts[0] = "get";
318
- }
319
- if (!isMethod(parts[0])) {
320
- parts.unshift(method);
321
- }
322
- if (parts[0] === method) {
323
- parts.shift(); // remove the method
324
- if (!parts[0].startsWith(service.name)) {
325
- parts.unshift(service.name);
326
- }
327
- path_segments = parts;
328
- }
329
- }
330
- if (parts.length > 1 && parts[0] === method) {
331
- parts.shift(); // remove the method
332
- if (!parts[0].startsWith(service.name)) {
333
- parts.unshift(service.name);
334
- }
335
- path_segments = parts;
336
- }
337
- }
338
- // combined the path & method into a name.
339
- if (path_segments.length === 0) {
340
- if (path.endsWith(".json")) {
341
- path = path.slice(0, -5);
342
- }
343
- path_segments = path.split("/").filter(segment => {
344
- return segment != "" && !segment.includes("{");
345
- });
346
- }
347
- const parts = [method.toLowerCase(), ...path_segments];
348
- return {
349
- snake: parts.join("_"),
350
- pascal: parts.map(s => s.charAt(0).toUpperCase() + s.substr(1)).join(''),
351
- };
352
- }
353
-
354
- /**
355
- * Check if the any response has caching enabled.
356
- */
357
- private hasCacheControl(op: OperationDetails): boolean {
358
- const responses = Object.values(op.obj.responses ?? {});
359
- return responses.some(response => "x-cache-control" in response);
360
- }
361
-
362
- /**
363
- * Find all the path parameter names.
364
- */
365
- private findPathParams(path: string): string[] {
366
- const matches = path.matchAll(/{[^}]*}/g);
367
- return Array.from(matches).map((match) => {
368
- return match[0].slice(1, -1);
369
- });
370
- }
371
-
372
- /**
373
- * Create a Response type by combining the Query, Path, and Body types.
374
- */
375
- private toRequestSchema(name: OperationName, details: OperationDetails): Schema {
376
- const request = new Schema("empty");
377
- if (!details.params.body.isEmpty()) {
378
- const body = new Schema(`${name.pascal}Body`);
379
- body.required = true;
380
- request.setProperty("body", body);
381
- }
382
- if (!details.params.query.isEmpty()) {
383
- request.merge(new Schema(`RequestQuery<${name.pascal}Query>`));
384
- }
385
- if (!details.params.path.isEmpty()) {
386
- request.merge(new Schema(`${name.pascal}Path`));
387
- }
388
- return request;
389
- }
390
- }
391
-
392
-
393
-
394
- /**
395
- * Main entry point.
396
- */
397
- async function main() {
398
- const { argv } = yargs(hideBin(process.argv))
399
- .options("manifest", {
400
- type: "string",
401
- description: "manifest.json file to generate sdk from",
402
- })
403
- .options("swagger", {
404
- type: "string",
405
- description: "swagger.json file to generate interfaces from",
406
- })
407
- .options("service", {
408
- type: "string",
409
- description: "service name for generated interfaces",
410
- })
411
- .options("ambient", {
412
- type: "boolean",
413
- description: "wrap the types in a declare module directive",
414
- default: true,
415
- })
416
- // generate sdk from manifest
417
- if (argv.manifest) {
418
- const data = await fs.promises.readFile(argv.manifest, "utf-8");
419
- const manifest: Manifest = JSON.parse(data);
420
- const gen = new CodeGenerator();
421
- await gen.manifest(manifest);
422
- return;
423
- }
424
- // generate interfaces for a single swagger.json
425
- if (argv.swagger || argv.service) {
426
- if (!argv.swagger) {
427
- throw new Error(`--swagger must be provided with --service`);
428
- }
429
- if (!argv.service) {
430
- throw new Error(`--service must be provided with --swagger`);
431
- }
432
- const gen = new CodeGenerator();
433
- const code = await gen.service({
434
- name: argv.service,
435
- swagger: argv.swagger,
436
- }, argv.ambient);
437
- console.log(code);
438
- return;
439
- }
440
- }
441
-
442
- main().catch(err => console.error(err.message));
package/template.ejs DELETED
@@ -1,21 +0,0 @@
1
-
2
- import { BaseServiceClient, RequestOptions, ResponsePromise } from "./base";
3
- export * from "./base";
4
-
5
- export class ServiceClient extends BaseServiceClient {
6
-
7
- <% for (const {comment, name, params, args, response} of methods) { %>
8
- /**
9
- * <%- comment %>
10
- <% if (params.length > 0) {-%>
11
- *
12
- <% } -%>
13
- <% for (const param of params) { -%>
14
- * @param <%- param.name %><%- param.description ? " - " + param.description : "" %>
15
- <% } -%>
16
- */
17
- <%- name %>(<% for (let i = 0; i < params.length; i++) { %><%- i > 0 ? ", " : "" %><%- params[i].name %><%= params[i].required ? "" : "?" %>: <%- params[i].type %><% } %>): ResponsePromise<<%= response %>> {
18
- return this.request(<% for (let i = 0; i < args.length; i++) { %><%- i > 0 ? ", " : "" %><%- args[i] %><% } %>);
19
- }
20
- <% } %>
21
- }
package/test/gen.test.ts DELETED
@@ -1,22 +0,0 @@
1
-
2
- import { OperationObject, OperationParams, Schema } from "@icholy/openapi-ts";
3
- import { CodeGenerator } from "../gen";
4
-
5
- describe("CodeGenerator", () => {
6
- describe("inferName", () => {
7
- const gen = new CodeGenerator();
8
- test("/order/location/brand/{id}", () => {
9
- const op: OperationObject = {
10
- operationId: "get_brand_orders",
11
- }
12
- const name = gen.inferName({
13
- method: "get",
14
- params: new OperationParams(op),
15
- path: "/order/location/brand/{id}",
16
- obj: op,
17
- }, { name: "order", swagger: "" });
18
- expect(name.pascal).toBe("GetOrderLocationBrand");
19
- expect(name.snake).toBe("get_order_location_brand");
20
- });
21
- });
22
- });