@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 +13 -0
- package/config/log4js.json +20 -0
- package/dist/cli.mjs +4 -0
- package/dist/components-parse.js +15 -0
- package/dist/index.js +31 -0
- package/dist/method.js +93 -0
- package/dist/openapi.js +1 -0
- package/dist/parameter.js +40 -0
- package/dist/property.js +27 -0
- package/dist/renderer.js +21 -0
- package/dist/schemas.js +45 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -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,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
|
+
}
|
package/dist/openapi.js
ADDED
|
@@ -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
|
+
}
|
package/dist/property.js
ADDED
|
@@ -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
|
+
}
|
package/dist/renderer.js
ADDED
|
@@ -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
|
+
}
|
package/dist/schemas.js
ADDED
|
@@ -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
|
+
}
|