@penkov/swagger-code-gen 1.0.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # swagger-code-gen
2
+ The utility to generate a client for a services, described with openapi.
3
+
4
+ Install:
5
+ ```shell
6
+ npm install -D @penkov/swagger-code-gen
7
+ ```
8
+
9
+
10
+ Usage:
11
+ ```shell
12
+ generate-client --url <URI> output_filename.ts
13
+ ```
@@ -0,0 +1,20 @@
1
+ {
2
+ "appenders": {
3
+ "out": {
4
+ "type": "stdout",
5
+ "layout": {
6
+ "type": "pattern",
7
+ "pattern": "%[%d [%p] (%f{1}:%l:%o) %c%] %m"
8
+ }
9
+ }
10
+ },
11
+ "categories": {
12
+ "default": {
13
+ "appenders": [
14
+ "out"
15
+ ],
16
+ "level": "DEBUG",
17
+ "enableCallStack": true
18
+ }
19
+ }
20
+ }
package/dist/cli.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './index.js';
3
+ main().then(() => {
4
+ });
@@ -0,0 +1,15 @@
1
+ import { Collection } from 'scats';
2
+ import { SchemaFactory } from './schemas.js';
3
+ import { Method } from './method.js';
4
+ export function resolveSchemas(json) {
5
+ const jsonSchemas = json.components.schemas;
6
+ return Collection.from(Object.keys(jsonSchemas))
7
+ .toMap(name => [name, SchemaFactory.build(name, jsonSchemas[name])]);
8
+ }
9
+ export function resolvePaths(json) {
10
+ const jsonSchemas = json.paths;
11
+ return Collection.from(Object.keys(jsonSchemas)).flatMap(path => {
12
+ const methods = jsonSchemas[path];
13
+ return Collection.from(Object.keys(methods)).map(methodName => new Method(path, methodName, methods[methodName]));
14
+ });
15
+ }
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ import log4js from 'log4js';
3
+ import fetch from 'node-fetch';
4
+ import { Renderer } from './renderer.js';
5
+ import { resolvePaths, resolveSchemas } from './components-parse.js';
6
+ export async function main() {
7
+ const { configure, getLogger } = log4js;
8
+ configure('./config/log4js.json');
9
+ const logger = getLogger('Main');
10
+ const program = new Command();
11
+ program
12
+ .name('Swagger client code generator')
13
+ .description('CLI to generate client based on swagger definitions')
14
+ .version('1.0.0')
15
+ .option('--url <URI>', 'The url with swagger definitions')
16
+ .argument('outputFile', 'File with generated code')
17
+ .parse();
18
+ const url = program.opts().url;
19
+ const outputFile = program.args[0];
20
+ logger.info(`Generating code from ${url}`);
21
+ const renderer = new Renderer();
22
+ fetch(url)
23
+ .then(res => res.json())
24
+ .then(async (json) => {
25
+ const schemas = resolveSchemas(json);
26
+ const paths = resolvePaths(json);
27
+ logger.debug(`Downloaded swagger: ${schemas.size} schemas, ${paths.size} paths`);
28
+ await renderer.renderToFile(schemas.values, paths, outputFile);
29
+ logger.debug(`Wrote client to ${outputFile}`);
30
+ });
31
+ }
package/dist/method.js ADDED
@@ -0,0 +1,93 @@
1
+ import { Collection, HashMap, identity, option } from 'scats';
2
+ import { Property } from './property.js';
3
+ import { Parameter } from './parameter.js';
4
+ import { SchemaFactory } from './schemas.js';
5
+ const sortByIn = HashMap.of(['path', 0], ['query', 1], ['header', 2], ['body', 3]);
6
+ export class Method {
7
+ constructor(path, method, def) {
8
+ this.path = path;
9
+ this.method = method;
10
+ this.tags = option(def.tags).getOrElseValue([]);
11
+ this.summary = def.summary;
12
+ const parameters = Collection.from(def.parameters)
13
+ .map(p => Parameter.fromDefinition(p))
14
+ .sort((a, b) => {
15
+ const r1 = a.required ? 1 : 0;
16
+ const r2 = b.required ? 1 : 0;
17
+ const reqS = r2 - r1;
18
+ if (reqS === 0) {
19
+ return sortByIn.get(a.in).getOrElseValue(10) - sortByIn.get(b.in).getOrElseValue(10);
20
+ }
21
+ else {
22
+ return reqS;
23
+ }
24
+ });
25
+ const namesCount = parameters.groupBy(p => p.name);
26
+ this.parameters = parameters.map(p => {
27
+ if (namesCount.get(p.name).exists(c => c.size > 1)) {
28
+ return p.copy({
29
+ uniqueName: `${p.in}${Method.capitalize(p.name)}`
30
+ });
31
+ }
32
+ else {
33
+ return p;
34
+ }
35
+ });
36
+ this.body = option(def.requestBody).flatMap(body => option(body.content))
37
+ .flatMap(body => {
38
+ const mimeTypes = Collection.from(Object.keys(body));
39
+ return mimeTypes
40
+ .find(_ => _ === 'application/json')
41
+ .orElseValue(mimeTypes.headOption)
42
+ .map(mt => SchemaFactory.build('body', body[mt].schema));
43
+ });
44
+ const statusCodes = Collection.from(Object.keys(def.responses))
45
+ .map(x => parseInt(x));
46
+ const successCode = statusCodes
47
+ .filter(code => code / 100 === 2)
48
+ .minByOption(identity);
49
+ const respDef = successCode.map(_ => def.responses[_]).getOrElseValue(def.responses[statusCodes.head]);
50
+ const mimeTypes = option(respDef.content)
51
+ .map(content => Collection.from(Object.keys(content)).toMap(mimeType => [mimeType, content[mimeType]])).getOrElseValue(HashMap.empty);
52
+ this.response = mimeTypes.get('application/json')
53
+ .orElseValue(mimeTypes.values.headOption)
54
+ .map(p => new Property('', p.schema))
55
+ .map(r => ({
56
+ responseType: r.jsType,
57
+ description: respDef.description
58
+ }))
59
+ .getOrElseValue(({
60
+ responseType: 'any'
61
+ }));
62
+ }
63
+ get endpointName() {
64
+ return `${this.method}${Method.pathToName(this.path)}`;
65
+ }
66
+ get pathWithSubstitutions() {
67
+ const paramPrefix = `${this.parameters.size > 2 ? 'params.' : ''}`;
68
+ return this.path.replace(/\{(\w+?)\}/g, (matched, group) => {
69
+ const remappedName = this.parameters.find(p => p.name === group && p.in === 'path')
70
+ .map(_ => _.uniqueName)
71
+ .getOrElseValue(group);
72
+ return `\${${paramPrefix}${remappedName}}`;
73
+ });
74
+ }
75
+ static pathToName(path) {
76
+ const tokens = Collection.from(path.split('/'));
77
+ return tokens.filter(t => t.length > 0).map(t => {
78
+ let token = t;
79
+ if (t[0] == '{') {
80
+ token = `By${this.capitalize(t.substring(1, t.length - 1))}`;
81
+ }
82
+ return Collection.from(token.split(/\W/)).map(_ => this.capitalize(_)).mkString();
83
+ }).mkString();
84
+ }
85
+ static capitalize(s) {
86
+ if (s.length <= 0) {
87
+ return s;
88
+ }
89
+ else {
90
+ return s[0].toUpperCase() + s.substring(1);
91
+ }
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { SchemaEnum, SchemaFactory, SchemaObject } from './schemas.js';
2
+ import { Collection, identity, option } from 'scats';
3
+ import { Method } from './method.js';
4
+ export class Parameter {
5
+ constructor(name, uniqueName, inValue, jsType, required, description) {
6
+ this.name = name;
7
+ this.uniqueName = uniqueName;
8
+ this.jsType = jsType;
9
+ this.required = required;
10
+ this.description = description;
11
+ this.in = inValue;
12
+ }
13
+ static fromDefinition(def) {
14
+ const name = Parameter.toJSName(def.name);
15
+ const inValue = def.in;
16
+ const desc = option(def.description);
17
+ const required = option(def.required).exists(identity);
18
+ const schema = SchemaFactory.build(def.name, def.schema);
19
+ let jsType;
20
+ if (schema instanceof SchemaObject) {
21
+ jsType = schema.type;
22
+ }
23
+ else if (schema instanceof SchemaEnum) {
24
+ jsType = schema.name;
25
+ }
26
+ else {
27
+ jsType = schema.jsType;
28
+ }
29
+ return new Parameter(name, name, inValue, jsType, required, desc);
30
+ }
31
+ static toJSName(path) {
32
+ const tokens = Collection.from(path.split(/\W/)).filter(t => t.length > 0);
33
+ return tokens.headOption.getOrElseValue('') + tokens.drop(1).map(t => {
34
+ return Method.capitalize(t);
35
+ }).mkString();
36
+ }
37
+ copy(p) {
38
+ return new Parameter(option(p.name).getOrElseValue(this.name), option(p.uniqueName).getOrElseValue(this.uniqueName), option(p.in).getOrElseValue(this.in), option(p.jsType).getOrElseValue(this.jsType), option(p.required).getOrElseValue(this.required), option(p.description).getOrElseValue(this.description));
39
+ }
40
+ }
@@ -0,0 +1,27 @@
1
+ import { identity, option } from 'scats';
2
+ const SCHEMA_PREFIX = '#/components/schemas/';
3
+ export class Property {
4
+ constructor(name, definition) {
5
+ this.name = name;
6
+ this.type = option(definition.$ref)
7
+ .map(ref => ref.substring(SCHEMA_PREFIX.length))
8
+ .getOrElseValue(definition.type);
9
+ this.nullable = option(definition.nullable).exists(identity);
10
+ this.description = option(definition.description);
11
+ this.required = option(definition.required).exists(identity);
12
+ this.items = option(definition.items?.$ref)
13
+ .map(ref => ref.substring(SCHEMA_PREFIX.length))
14
+ .orElseValue(option(definition.items?.type))
15
+ .getOrElseValue('any');
16
+ }
17
+ get jsType() {
18
+ return Property.toJsType(this.type, this.items);
19
+ }
20
+ static toJsType(tpe, itemTpe = 'any') {
21
+ switch (tpe) {
22
+ case 'integer': return 'number';
23
+ case 'array': return `readonly ${Property.toJsType(itemTpe)}[]`;
24
+ default: return tpe;
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,21 @@
1
+ import * as ejs from 'ejs';
2
+ import * as fs from 'fs';
3
+ export class Renderer {
4
+ async renderToFile(schemas, methods, file) {
5
+ const view = await ejs.renderFile('./src/templates/index.ejs', {
6
+ schemas: schemas,
7
+ methods: methods,
8
+ });
9
+ fs.writeFileSync(file, view);
10
+ }
11
+ async renderSchema(obj) {
12
+ return await ejs.renderFile('./src/templates/index.ejs', {
13
+ schemas: obj
14
+ });
15
+ }
16
+ async renderMethod(obj) {
17
+ return (await obj.mapPromise(m => ejs.renderFile('./src/templates/method.ejs', {
18
+ method: m
19
+ }))).mkString('\n');
20
+ }
21
+ }
@@ -0,0 +1,45 @@
1
+ import { Collection, Nil, option } from 'scats';
2
+ import { Property } from './property.js';
3
+ export class SchemaFactory {
4
+ static build(name, def) {
5
+ if (def.type === 'object') {
6
+ return new SchemaObject(name, def);
7
+ }
8
+ else if (def.enum) {
9
+ return new SchemaEnum(name, def);
10
+ }
11
+ else if (def.type === 'string') {
12
+ return new Property(name, def);
13
+ }
14
+ else if (def.type === 'boolean') {
15
+ return new Property(name, def);
16
+ }
17
+ else if (def.type === 'integer') {
18
+ return new Property(name, def);
19
+ }
20
+ else if (def.type === 'array') {
21
+ return new Property(name, def);
22
+ }
23
+ else {
24
+ return new Property(name, def);
25
+ }
26
+ }
27
+ }
28
+ export class SchemaEnum {
29
+ constructor(name, def) {
30
+ this.schemaType = 'enum';
31
+ this.name = name;
32
+ this.title = def.title;
33
+ this.type = def.type;
34
+ this.values = option(def.enum).map(Collection.from).getOrElseValue(Nil);
35
+ }
36
+ }
37
+ export class SchemaObject {
38
+ constructor(name, def) {
39
+ this.schemaType = 'object';
40
+ this.name = name;
41
+ this.title = def.title;
42
+ this.type = def.type;
43
+ this.properties = Collection.from(Object.keys(def.properties)).map(p => new Property(p, def.properties[p]));
44
+ }
45
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@penkov/swagger-code-gen",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "generate-client": "./dist/cli.mjs"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/papirosko/swagger-code-gen"
11
+ },
12
+ "keywords": [
13
+ "swagger",
14
+ "openapi",
15
+ "typescript",
16
+ "code generator"
17
+ ],
18
+ "author": "penkov.vladimir@gmail.com",
19
+ "license": "ISC",
20
+ "bugs": {
21
+ "url": "https://github.com/papirosko/swagger-code-gen/issues"
22
+ },
23
+ "homepage": "https://github.com/papirosko/swagger-code-gen#readme",
24
+ "scripts": {
25
+ "clean": "rimraf dist",
26
+ "lint": "eslint \"{src,test}/**/*.ts\" --fix",
27
+ "prebuild": "npm run lint && npm run clean",
28
+ "build": "tsc && chmod +x ./dist/cli.mjs"
29
+ },
30
+ "dependencies": {
31
+ "commander": "^9.4.1",
32
+ "ejs": "^3.1.8",
33
+ "log4js": "^6.7.1",
34
+ "node-fetch": "^3.3.0",
35
+ "scats": "^1.3.0",
36
+ "ts-node": "^10.9.1",
37
+ "tslib": "^2.4.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/ejs": "^3.1.1",
41
+ "@typescript-eslint/eslint-plugin": "^5.10.2",
42
+ "@typescript-eslint/parser": "^5.10.2",
43
+ "eslint": "^7.30.0",
44
+ "rimraf": "^3.0.2",
45
+ "typescript": "^4.9.3"
46
+ }
47
+ }