@penkov/swagger-code-gen 1.0.7 → 1.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/README.md CHANGED
@@ -11,3 +11,8 @@ Usage:
11
11
  ```shell
12
12
  generate-client --url <URI> output_filename.ts
13
13
  ```
14
+
15
+ Cli parameters:
16
+ * `--url` - the swagger url
17
+ * `--enableScats` - generate additional wrappers in [scats](https://www.npmjs.com/package/scats)
18
+ style for all objects and create a service with methods for each endpoint.
package/dist/cli.mjs CHANGED
@@ -1,4 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from './index.js';
3
- main().then(() => {
3
+ import { Command } from "commander";
4
+ const program = new Command();
5
+ program
6
+ .name('Swagger client code generator')
7
+ .description('CLI to generate client based on swagger definitions')
8
+ .version('1.0.0')
9
+ .option('--url <URI>', 'The url with swagger definitions')
10
+ .option('--enableScats', 'Generate scats', false)
11
+ .argument('outputFile', 'File with generated code')
12
+ .parse();
13
+ const url = program.opts().url;
14
+ const enableScats = program.opts().enableScats;
15
+ const outputFile = program.args[0];
16
+ main(url, enableScats, outputFile).then(() => {
4
17
  });
@@ -1,15 +1,21 @@
1
1
  import { Collection } from 'scats';
2
2
  import { SchemaFactory } from './schemas.js';
3
3
  import { Method } from './method.js';
4
- export function resolveSchemas(json) {
4
+ export function resolveSchemasTypes(json) {
5
5
  const jsonSchemas = json.components.schemas;
6
- return Collection.from(Object.keys(jsonSchemas))
7
- .toMap(name => [name, SchemaFactory.build(name, jsonSchemas[name])]);
6
+ const schemasNames = Collection.from(Object.keys(jsonSchemas));
7
+ return schemasNames.toMap(name => [name, SchemaFactory.resolveSchemaType(jsonSchemas[name])]);
8
8
  }
9
- export function resolvePaths(json) {
9
+ export function resolveSchemas(json, schemasTypes) {
10
+ const jsonSchemas = json.components.schemas;
11
+ const schemasNames = Collection.from(Object.keys(jsonSchemas));
12
+ return schemasNames
13
+ .toMap(name => [name, SchemaFactory.build(name, jsonSchemas[name], schemasTypes)]);
14
+ }
15
+ export function resolvePaths(json, schemasTypes) {
10
16
  const jsonSchemas = json.paths;
11
17
  return Collection.from(Object.keys(jsonSchemas)).flatMap(path => {
12
18
  const methods = jsonSchemas[path];
13
- return Collection.from(Object.keys(methods)).map(methodName => new Method(path, methodName, methods[methodName]));
19
+ return Collection.from(Object.keys(methods)).map(methodName => new Method(path, methodName, methods[methodName], schemasTypes));
14
20
  });
15
21
  }
package/dist/index.js CHANGED
@@ -1,35 +1,25 @@
1
- import { Command } from 'commander';
2
1
  import log4js from 'log4js';
3
2
  import fetch from 'node-fetch';
4
3
  import { Renderer } from './renderer.js';
5
- import { resolvePaths, resolveSchemas } from './components-parse.js';
4
+ import { resolvePaths, resolveSchemas, resolveSchemasTypes } from './components-parse.js';
6
5
  import { fileURLToPath } from 'url';
7
6
  import { dirname } from 'path';
8
7
  const __filename = fileURLToPath(import.meta.url);
9
8
  const __dirname = dirname(__filename);
10
- export async function main() {
9
+ export async function main(url, enableScats, outputFile) {
11
10
  const { configure, getLogger } = log4js;
12
11
  configure(`${__dirname}/../config/log4js.json`);
13
12
  const logger = getLogger('Generator');
14
- const program = new Command();
15
- program
16
- .name('Swagger client code generator')
17
- .description('CLI to generate client based on swagger definitions')
18
- .version('1.0.0')
19
- .option('--url <URI>', 'The url with swagger definitions')
20
- .argument('outputFile', 'File with generated code')
21
- .parse();
22
- const url = program.opts().url;
23
- const outputFile = program.args[0];
24
13
  logger.info(`Generating code from ${url}`);
25
14
  const renderer = new Renderer();
26
15
  fetch(url)
27
16
  .then(res => res.json())
28
17
  .then(async (json) => {
29
- const schemas = resolveSchemas(json);
30
- const paths = resolvePaths(json);
18
+ const schemasTypes = resolveSchemasTypes(json);
19
+ const schemas = resolveSchemas(json, schemasTypes);
20
+ const paths = resolvePaths(json, schemasTypes);
31
21
  logger.debug(`Downloaded swagger: ${schemas.size} schemas, ${paths.size} paths`);
32
- await renderer.renderToFile(schemas.values, paths, outputFile);
22
+ await renderer.renderToFile(schemas.values, paths, enableScats, outputFile);
33
23
  logger.debug(`Wrote client to ${outputFile}`);
34
24
  });
35
25
  }
package/dist/method.js CHANGED
@@ -4,13 +4,13 @@ import { Parameter } from './parameter.js';
4
4
  import { SchemaFactory } from './schemas.js';
5
5
  const sortByIn = HashMap.of(['path', 0], ['query', 1], ['header', 2], ['body', 3]);
6
6
  export class Method {
7
- constructor(path, method, def) {
7
+ constructor(path, method, def, schemasTypes) {
8
8
  this.path = path;
9
9
  this.method = method;
10
10
  this.tags = option(def.tags).getOrElseValue([]);
11
11
  this.summary = def.summary;
12
12
  const parameters = Collection.from(def.parameters)
13
- .map(p => Parameter.fromDefinition(p))
13
+ .map(p => Parameter.fromDefinition(p, schemasTypes))
14
14
  .sort((a, b) => {
15
15
  const r1 = a.required ? 1 : 0;
16
16
  const r2 = b.required ? 1 : 0;
@@ -39,7 +39,7 @@ export class Method {
39
39
  return mimeTypes
40
40
  .find(_ => _ === 'application/json')
41
41
  .orElseValue(mimeTypes.headOption)
42
- .map(mt => SchemaFactory.build('body', body[mt].schema));
42
+ .map(mt => SchemaFactory.build('body', body[mt].schema, schemasTypes));
43
43
  });
44
44
  const statusCodes = Collection.from(Object.keys(def.responses))
45
45
  .map(x => parseInt(x));
@@ -51,12 +51,14 @@ export class Method {
51
51
  .map(content => Collection.from(Object.keys(content)).toMap(mimeType => [mimeType, content[mimeType]])).getOrElseValue(HashMap.empty);
52
52
  this.response = mimeTypes.get('application/json')
53
53
  .orElseValue(mimeTypes.values.headOption)
54
- .map(p => new Property('', p.schema))
54
+ .map(p => Property.fromDefinition('', p.schema, schemasTypes))
55
55
  .map(r => ({
56
+ asProperty: r,
56
57
  responseType: r.jsType,
57
58
  description: respDef.description
58
59
  }))
59
60
  .getOrElseValue(({
61
+ asProperty: Property.fromDefinition('UNKNOWN', { type: 'any' }, schemasTypes),
60
62
  responseType: 'any'
61
63
  }));
62
64
  }
package/dist/parameter.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SchemaEnum, SchemaFactory, SchemaObject } from './schemas.js';
2
2
  import { Collection, identity, option } from 'scats';
3
3
  import { Method } from './method.js';
4
+ import { Property } from './property.js';
4
5
  export class Parameter {
5
6
  constructor(name, uniqueName, inValue, jsType, required, description) {
6
7
  this.name = name;
@@ -10,12 +11,12 @@ export class Parameter {
10
11
  this.description = description;
11
12
  this.in = inValue;
12
13
  }
13
- static fromDefinition(def) {
14
+ static fromDefinition(def, schemas) {
14
15
  const name = Parameter.toJSName(def.name);
15
16
  const inValue = def.in;
16
17
  const desc = option(def.description);
17
18
  const required = option(def.required).exists(identity);
18
- const schema = SchemaFactory.build(def.name, def.schema);
19
+ const schema = SchemaFactory.build(def.name, def.schema, schemas);
19
20
  let jsType;
20
21
  if (schema instanceof SchemaObject) {
21
22
  jsType = schema.type;
@@ -23,9 +24,12 @@ export class Parameter {
23
24
  else if (schema instanceof SchemaEnum) {
24
25
  jsType = schema.name;
25
26
  }
26
- else {
27
+ else if (schema instanceof Property) {
27
28
  jsType = schema.jsType;
28
29
  }
30
+ else {
31
+ throw new Error('Unknown schema type');
32
+ }
29
33
  return new Parameter(name, name, inValue, jsType, required, desc);
30
34
  }
31
35
  static toJSName(path) {
package/dist/property.js CHANGED
@@ -1,22 +1,42 @@
1
1
  import { identity, option } from 'scats';
2
2
  const SCHEMA_PREFIX = '#/components/schemas/';
3
3
  export class Property {
4
- constructor(name, definition) {
4
+ constructor(name, type, description, defaultValue, nullable, required, items, referencesObject, itemReferencesObject) {
5
5
  this.name = name;
6
- this.type = option(definition.$ref)
6
+ this.type = type;
7
+ this.description = description;
8
+ this.defaultValue = defaultValue;
9
+ this.nullable = nullable;
10
+ this.required = required;
11
+ this.items = items;
12
+ this.referencesObject = referencesObject;
13
+ this.itemReferencesObject = itemReferencesObject;
14
+ this.schemaType = 'property';
15
+ }
16
+ static fromDefinition(name, definition, schemas) {
17
+ const referencesObject = option(definition.$ref)
18
+ .exists(ref => schemas.get(ref.substring(SCHEMA_PREFIX.length)).contains('object'));
19
+ const itemReferencesObject = option(definition.items)
20
+ .flatMap(i => option(i.$ref))
21
+ .exists(ref => schemas.get(ref.substring(SCHEMA_PREFIX.length)).contains('object'));
22
+ const type = option(definition.$ref)
7
23
  .map(ref => ref.substring(SCHEMA_PREFIX.length))
8
24
  .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)
25
+ const nullable = option(definition.nullable).exists(identity);
26
+ const description = option(definition.description);
27
+ const required = option(definition.required).exists(identity);
28
+ const items = option(definition.items?.$ref)
13
29
  .map(ref => ref.substring(SCHEMA_PREFIX.length))
14
30
  .orElseValue(option(definition.items?.type))
15
31
  .getOrElseValue('any');
32
+ return new Property(name, type, description, null, nullable, required, items, referencesObject, itemReferencesObject);
16
33
  }
17
34
  get jsType() {
18
35
  return Property.toJsType(this.type, this.items);
19
36
  }
37
+ get isArray() {
38
+ return this.type === 'array';
39
+ }
20
40
  static toJsType(tpe, itemTpe = 'any') {
21
41
  switch (tpe) {
22
42
  case 'integer': return 'number';
@@ -24,4 +44,28 @@ export class Property {
24
44
  default: return tpe;
25
45
  }
26
46
  }
47
+ get itemScatsWrapperType() {
48
+ if (this.isArray) {
49
+ if (this.itemReferencesObject) {
50
+ return `${this.items}Dto`;
51
+ }
52
+ else {
53
+ return Property.toJsType(this.items);
54
+ }
55
+ }
56
+ else {
57
+ return this.scatsWrapperType;
58
+ }
59
+ }
60
+ get scatsWrapperType() {
61
+ if (this.referencesObject) {
62
+ return this.nullable ? `Option<${this.type}Dto>` : `${this.type}Dto`;
63
+ }
64
+ else if (this.isArray) {
65
+ return this.itemReferencesObject ? `Collection<${this.items}Dto>` : `Collection<${Property.toJsType(this.items)}>`;
66
+ }
67
+ else {
68
+ return this.nullable ? `Option<${this.jsType}>` : this.jsType;
69
+ }
70
+ }
27
71
  }
package/dist/renderer.js CHANGED
@@ -6,10 +6,11 @@ import { dirname } from 'path';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
8
8
  export class Renderer {
9
- async renderToFile(schemas, methods, file) {
9
+ async renderToFile(schemas, methods, enableScats, file) {
10
10
  const view = await ejs.renderFile(path.resolve(__dirname, 'templates/index.ejs'), {
11
11
  schemas: schemas,
12
12
  methods: methods,
13
+ scats: enableScats
13
14
  });
14
15
  fs.writeFileSync(file, view);
15
16
  }
package/dist/schemas.js CHANGED
@@ -1,45 +1,63 @@
1
1
  import { Collection, Nil, option } from 'scats';
2
2
  import { Property } from './property.js';
3
3
  export class SchemaFactory {
4
- static build(name, def) {
4
+ static resolveSchemaType(def) {
5
5
  if (def.type === 'object') {
6
- return new SchemaObject(name, def);
6
+ return 'object';
7
7
  }
8
8
  else if (def.enum) {
9
- return new SchemaEnum(name, def);
9
+ return 'enum';
10
+ }
11
+ else {
12
+ return 'property';
13
+ }
14
+ }
15
+ static build(name, def, schemasTypes) {
16
+ if (def.type === 'object') {
17
+ return SchemaObject.fromDefinition(name, def, schemasTypes);
18
+ }
19
+ else if (def.enum) {
20
+ return SchemaEnum.fromDefinition(name, def);
10
21
  }
11
22
  else if (def.type === 'string') {
12
- return new Property(name, def);
23
+ return Property.fromDefinition(name, def, schemasTypes);
13
24
  }
14
25
  else if (def.type === 'boolean') {
15
- return new Property(name, def);
26
+ return Property.fromDefinition(name, def, schemasTypes);
16
27
  }
17
28
  else if (def.type === 'integer') {
18
- return new Property(name, def);
29
+ return Property.fromDefinition(name, def, schemasTypes);
19
30
  }
20
31
  else if (def.type === 'array') {
21
- return new Property(name, def);
32
+ return Property.fromDefinition(name, def, schemasTypes);
22
33
  }
23
34
  else {
24
- return new Property(name, def);
35
+ return Property.fromDefinition(name, def, schemasTypes);
25
36
  }
26
37
  }
27
38
  }
28
39
  export class SchemaEnum {
29
- constructor(name, def) {
30
- this.schemaType = 'enum';
40
+ constructor(name, title, type, values) {
31
41
  this.name = name;
32
- this.title = def.title;
33
- this.type = def.type;
34
- this.values = option(def.enum).map(Collection.from).getOrElseValue(Nil);
42
+ this.title = title;
43
+ this.type = type;
44
+ this.values = values;
45
+ this.schemaType = 'enum';
46
+ }
47
+ static fromDefinition(name, def) {
48
+ return new SchemaEnum(name, def.title, def.type, option(def.enum).map(Collection.from).getOrElseValue(Nil));
35
49
  }
36
50
  }
37
51
  export class SchemaObject {
38
- constructor(name, def) {
39
- this.schemaType = 'object';
52
+ constructor(name, title, type, properties) {
40
53
  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]));
54
+ this.title = title;
55
+ this.type = type;
56
+ this.properties = properties;
57
+ this.schemaType = 'object';
58
+ }
59
+ static fromDefinition(name, def, schemasTypes) {
60
+ const properties = Collection.from(Object.keys(def.properties)).map(p => Property.fromDefinition(p, def.properties[p], schemasTypes));
61
+ return new SchemaObject(name, def.title, def.type, properties);
44
62
  }
45
63
  }
@@ -11,30 +11,50 @@
11
11
  *********************************************************
12
12
  *********************************************************/
13
13
  import fetch from 'node-fetch';
14
+ <% if (scats) {%>
15
+ import {option, Option, Collection, Try, TryLike, none} from 'scats';
16
+ <% } %>
17
+
18
+ export interface RequestOptions {
19
+ headers: Record<string, string>;
20
+ }
21
+
14
22
 
15
23
  export interface SwaggerCodeGenParams {
16
24
  apiPrefix: string;
25
+ requestOptions: RequestOptions
17
26
  }
18
27
 
19
28
  export const swaggerCodeGenParams: SwaggerCodeGenParams = {
20
- apiPrefix: ''
29
+ apiPrefix: '',
30
+ requestOptions: {
31
+ headers: {
32
+ 'Accept': 'application/json',
33
+ 'Content-Type': 'application/json'
34
+ }
35
+ }
21
36
  };
22
37
 
38
+ const defaultRequestOptions = () => swaggerCodeGenParams.requestOptions;
39
+
23
40
  <% schemas.foreach(schema => { %>
24
41
  <%- include('schema.ejs', {schema: schema}); %>
25
42
  <% }); %>
26
43
 
27
- export interface RequestOptions {
28
- headers: Record<string, string>;
29
- }
30
44
 
31
- export const defaultRequestOptions = {
32
- headers: {
33
- 'Accept': 'application/json',
34
- 'Content-Type': 'application/json'
35
- }
36
- }
37
45
 
38
46
  <% methods.foreach(method => { %>
39
47
  <%- include('method.ejs', {method: method}); %>
40
48
  <% }); %>
49
+
50
+
51
+ <% if (scats) {%>
52
+ <% schemas.foreach(schema => { %>
53
+ <%- include('scats-schema.ejs', {schema: schema}); %>
54
+ <% }); %>
55
+
56
+ <%- include('scats-method.ejs'); %>
57
+
58
+ <% } %>
59
+
60
+
@@ -28,7 +28,7 @@ export async function <%= method.endpointName %>(
28
28
  <%_ if (method.body.nonEmpty) { -%>
29
29
  body: <%= method.body.get.type %>,
30
30
  <%_ } -%>
31
- requestOptions: RequestOptions = defaultRequestOptions
31
+ requestOptions: RequestOptions = defaultRequestOptions()
32
32
  ): Promise<<%= method.response.responseType %>> {
33
33
  let query = '';
34
34
  <%_ if (method.parameters.filter(x => x.in === 'query').nonEmpty) { -%>
@@ -0,0 +1,52 @@
1
+ export class ApiClient {
2
+
3
+ constructor(private readonly requestOptions: RequestOptions = swaggerCodeGenParams.requestOptions) {
4
+ }
5
+
6
+
7
+ <% methods.foreach(method => { %>
8
+ async <%= method.endpointName %>(
9
+ <%_ if (method.parameters.size <= 2) { -%>
10
+ <%_ method.parameters.foreach(p => { -%>
11
+ <%= p.uniqueName %>: <%- p.required && !p.nullable ? p.jsType : `Option<${p.jsType}> = none` %>,
12
+ <%_ }) -%>
13
+ <%_ } else { -%>
14
+ params: {
15
+ <%_ method.parameters.foreach(p => { -%>
16
+ <%= p.uniqueName %><%= p.required && !p.nullable ? '' : '?'%>: <%= p.jsType %>,
17
+ <%_ }) -%>
18
+ },
19
+ <%_ } -%>
20
+ <%_ method.body.foreach(body => { -%>
21
+ body: <%= body.type %>,
22
+ <%_ }); -%>
23
+ requestOptions: RequestOptions = this.requestOptions
24
+ ): Promise<TryLike<<%- method.response.asProperty.scatsWrapperType %>>> {
25
+ return (await Try.promise(() =>
26
+ <%= method.endpointName %>(
27
+ <%_ if (method.parameters.size <= 2) { -%>
28
+ <%_ method.parameters.foreach(p => { -%>
29
+ <%= p.uniqueName %><%- p.required && !p.nullable ? '' : `.orUndefined` %>,
30
+ <%_ }) -%>
31
+ <%_ } else { -%>
32
+ params,
33
+ <%_ } -%>
34
+ <%_ if (method.body.nonEmpty) { -%>
35
+ body,
36
+ <%_ } -%>
37
+ requestOptions
38
+ )
39
+ ))
40
+ <%_ if (method.response.asProperty.referencesObject) { _%>
41
+ .map(res => <%- method.response.asProperty.scatsWrapperType %>.fromJson(res))
42
+ <%_ } else if (method.response.asProperty.isArray) { _%>
43
+ .map(res => Collection.from(option(res).getOrElseValue([])))
44
+ <%_ if (method.response.asProperty.itemReferencesObject) { _%>
45
+ .map(items => items.map(i => <%- method.response.asProperty.itemScatsWrapperType %>.fromJson(i)))
46
+ <%_ } _%>
47
+ <%_ } _%>;
48
+ }
49
+ <% }); %>
50
+
51
+
52
+ }
@@ -0,0 +1,38 @@
1
+ <%_ if (schema.schemaType === 'object') { -%>
2
+ export class <%= schema.name %>Dto {
3
+
4
+ constructor(
5
+ <%_ schema.properties.foreach(p => { -%>
6
+ readonly <%= p.name %>: <%- p.scatsWrapperType %>,
7
+ <%_ }); -%>
8
+ ) {}
9
+
10
+
11
+ static fromJson(json: <%= schema.name %>): <%= schema.name %>Dto {
12
+ return new <%= schema.name %>Dto(
13
+ <%_ schema.properties.foreach(p => { _%>
14
+ <%_ if (p.referencesObject) { _%>
15
+ <%- p.scatsWrapperType %>.fromJson(json.<%= p.name %>),
16
+ <%_ } else if (p.isArray) { _%>
17
+ Collection.from(option(json.<%= p.name %>).getOrElseValue([]))
18
+ <% if (p.itemReferencesObject) { _%>.map(i => <%- p.itemScatsWrapperType %>.fromJson(i))<%_ } _%>,
19
+ <%_ } else if (p.required || !p.nullable) { _%>
20
+ json.<%= p.name %>,
21
+ <%_ } else { _%>
22
+ option(json.<%= p.name %>),
23
+ <%_ } _%>
24
+ <%_ }); _%>
25
+ );
26
+ }
27
+
28
+ copy(fields: Partial<<%= schema.name %>Dto>): <%= schema.name %>Dto {
29
+ return new <%= schema.name %>Dto(
30
+ <%_ schema.properties.foreach(p => { -%>
31
+ option(fields.<%= p.name %>).getOrElseValue(this.<%= p.name %>),
32
+ <%_ }); -%>
33
+ );
34
+ }
35
+ }
36
+
37
+ <%_ } -%>
38
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penkov/swagger-code-gen",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "generate-client": "./dist/cli.mjs"
@@ -25,7 +25,7 @@
25
25
  "clean": "rimraf dist",
26
26
  "lint": "eslint \"{src,test}/**/*.ts\" --fix",
27
27
  "prebuild": "npm run lint && npm run clean",
28
- "copy-files": "mkdir -p ./dist/templates && cp ./src/templates/* ./dist/templates/",
28
+ "copy-files": "mkdir -p ./dist/templates && cp -R ./src/templates/* ./dist/templates/",
29
29
  "build": "tsc && chmod +x ./dist/cli.mjs && npm run copy-files"
30
30
  },
31
31
  "dependencies": {
@@ -34,7 +34,6 @@
34
34
  "log4js": "^6.7.1",
35
35
  "node-fetch": "^3.3.0",
36
36
  "scats": "^1.3.0",
37
- "ts-node": "^10.9.1",
38
37
  "tslib": "^2.4.1"
39
38
  },
40
39
  "devDependencies": {
@@ -43,6 +42,7 @@
43
42
  "@typescript-eslint/parser": "^5.10.2",
44
43
  "eslint": "^7.30.0",
45
44
  "rimraf": "^3.0.2",
45
+ "ts-node": "^10.9.1",
46
46
  "typescript": "^4.9.3"
47
47
  }
48
48
  }