Skip to content

Commit

Permalink
Heavy refactoring of the generator.
Browse files Browse the repository at this point in the history
Now the code runs with ts-node no longer compiled with tsc.
  • Loading branch information
Luis Fernando Planella Gonzalez committed Feb 5, 2019
1 parent e719d4c commit be08452
Show file tree
Hide file tree
Showing 42 changed files with 39,378 additions and 167 deletions.
5 changes: 5 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
out
test
.directory
package-lock.json
87 changes: 78 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,83 @@
ng-openapi-gen: An OpenAPI 3 code generator for Angular
---

This project is a NPM module that generates model interfaces and web service
clients from an [OpenApi 3](https://www.openapis.org/)
[specification](https://github.com/OAI/OpenAPI-Specification).
The generated classes follow the principles of
[Angular](https://angular.io/). The generated code is compatible with Angular 6+.
This project is a NPM module that generates model interfaces and web service clients from an [OpenApi 3](https://www.openapis.org/) [specification](https://github.com/OAI/OpenAPI-Specification).
The generated classes follow the principles of [Angular](https://angular.io/).
The generated code is compatible with Angular 6+.

For a generator for [Swagger 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md),
use [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) instead.
For a generator for [Swagger 2.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md), use [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) instead.

`ng-openapi-gen` is still in early development stage, and is not yet
recommended for production.
`ng-openapi-gen` is still in early development stage, and is not yet recommended for production.

## Highlights

- It should be easy to use and to integrate with Angular CLI;
- It should support `OpenAPI` specifications in both `JSON` and `YAML` formats;
- Each tag in the OpenAPI specification generates an Angular `@Injectable()` service;
- An Angular `@NgModule()` is generated, which provides all services;
- It should be easy to access the original `HttpResponse`, for example, to read headers.
This is achieved by generating a variant suffixed with `$Response` for each generated method;
- `OpenAPI` supports combinations of request body and response content types.
For each combination, a distinct method is generated;
- It should be possible to specify a subset of services to generate.
Only the models actually used by that subset should be generated;
- It should be easy to specify a root URL for the web service endpoints;
- Generated files should compile using strict `TypeScript` compiler flags, such as `noUnusedLocals` and `noUnusedParameters`.

## Limitations

- Only standard OpenAPI 3 descriptions will be generated. `ng-swagger-gen` allows several extensions, specially types from JSON schema, but they are out of scope for `ng-openapi-gen`;
- Servers per operation are not supported;
- Only the first server is used as a default root URL in the configuration;
- No data transformation is ever performed before sending / after returning data.
This means that a property of type `string` and format `date-time` will always be generated as `string`.
Otherwise every API call would need to have a processing that would traverse the returned object graph before sending the request
to replace all date properties by `Date`. The same applies to sent requests. Such operations are out of scope for `ng-openapi-gen`;

## Relationship with ng-swagger-gen

This project uses the same philosophy as [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen), and was built by the same team.
We've learned a lot with `ng-swagger-gen` and have applied all the acuired knowledge to build `ng-openapi-gen`.

There were several reasons to not build a new major version of `ng-swagger-gen` that supports `OpenAPI 3`, but instead,
to create a new project.
The main differences between `ng-openapi-gen` and `ng-swagger-gen` are:

- The first, more obvious and more important is the specification version, `OpenAPI 3` vs `Swagger 2`;
- The generator itself is written in `TypeScript`, which should be easier to maintain;
- There is a test suite for the generator;
- The command-line arguments are more robust, derived directly from the `JSON schema` definition for the configuration file, easily
allowing to override any specific configuration on CLI.
- Root enumerations (schemas of `type` = `string` | `number` | `integer`) can be generated as TypeScript's `enum`'s.
This is enabled by default. Inline enums are not, because it would require another type to be exported in the container type.

## Installing and running

You may want to install `ng-openapi-gen` globally or just on your project. Here is an example for a global setup:

```bash
$ npm install -g ng-openapi-gen
$ ng-openapi-gen --input my-api.yaml --output my-app/src/app/api
```

This will expect the file `my-api.yaml` to be in the current directory, and will generate the files on `my-app/src/app/api`.

## Configuration file and CLI arguments

If the file `ng-openapi-gen.json` exists in the current directory, it will be read. Alternatively, you can run `ng-openapi-gen --config my-config.json` to specify a different configuration file.
The only required configuration property is `input`, which specified the `OpenAPI` specification file. The default `output` is `src/app/api`.

For a list with all possible configuration options, see the [JSON schema file](https://raw.githubusercontent.com/cyclosproject/ng-openapi-gen/master/ng-openapi-gen-schema.json).
You can also run `ng-openapi-gen --help` to see all available options.
Each option in the JSON schema can be passed in as a CLI argument, both in camel case, like `--includeTags tag1,tag2,tag3`, or in kebab case, like `--exclude-tags tag1,tag2,tag3`.

Here is an example of a configuration file:

```json
{
"$schema": "node_modules/ng-openapi-gen/ng-openapi-gen-schema.json",
"input": "my-file.json",
"output": "out/person-place",
"ignoreUnusedModels": false
}
```
58 changes: 32 additions & 26 deletions src/cmd-args.ts → lib/cmd-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import { kebabCase } from 'lodash';

const DefaultConfig = 'ng-openapi-gen.json';

const argParser = new ArgumentParser({
version: pkg.version,
addHelp: true,
description: `
function createParser() {
const argParser = new ArgumentParser({
version: pkg.version,
addHelp: true,
description: `
Generator for API clients described with OpenAPI 3.0 specification for
Angular 6+ projects. Requires a configuration file, which defaults to
${DefaultConfig} in the current directory. The file can also be
Expand All @@ -22,42 +23,47 @@ argument '--serviceSuffix Suffix'. Kebab-case is also accepted, so, the same
argument could be set as '--service-suffix Suffix'
As the only required argument is the input for OpenAPI specification,
a configuration file is only required if no --input argument is set.`.trim()
});
argParser.addArgument(
['-c', '--config'],
{
help: `
});
argParser.addArgument(
['-c', '--config'],
{
help: `
The configuration file to be used. If not specified, assumes that
${DefaultConfig} in the current directory`.trim(),
dest: 'config'
}
);
const props = schema.properties;
for (const key of Object.keys(props)) {
if (key === '$schema') {
continue;
}
const kebab = kebabCase(key);
const desc = (props as any)[key];
const names = ['--' + key];
if (kebab !== key) {
names.push('--' + kebab);
dest: 'config',
defaultValue: `./${DefaultConfig}`
}
);
const props = schema.properties;
for (const key of Object.keys(props)) {
if (key === '$schema') {
continue;
}
const kebab = kebabCase(key);
const desc = (props as any)[key];
const names = ['--' + key];
if (kebab !== key) {
names.push('--' + kebab);
}
argParser.addArgument(names, {
help: desc.description,
dest: key
});
}
argParser.addArgument(names, {
help: desc.description,
dest: key
});
return argParser;
}

/**
* Parses the options from command-line arguments
*/
export function parseOptions(sysArgs?: string[]): Options {
const argParser = createParser();
const args = argParser.parseArgs(sysArgs);
let options: any = {};
if (args.config) {
options = JSON.parse(fs.readFileSync(args.config, { encoding: 'utf-8' }));
}
const props = schema.properties;
for (const key of Object.keys(args)) {
let value = args[key];
if (key === 'config' || value == null) {
Expand Down
File renamed without changes.
21 changes: 21 additions & 0 deletions lib/enum-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import jsesc from 'jsesc';
import { enumName } from './gen-utils';
import { Options } from './options';

/**
* Represents a possible enumerated value
*/
export class EnumValue {
name: string;
value: string;

constructor(public type: string, _value: any, public options: Options) {
const value = String(_value);
this.name = enumName(value, options);
if (type === 'string') {
this.value = `'${jsesc(value)}'`;
} else {
this.value = value;
}
}
}
File renamed without changes.
35 changes: 28 additions & 7 deletions src/gen-utils.ts → lib/gen-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import jsesc from 'jsesc';
import { OpenAPIObject, ReferenceObject, SchemaObject } from 'openapi3-ts';
import { Options } from './options';
import { upperFirst, kebabCase, deburr, camelCase } from 'lodash';
import { upperFirst, kebabCase, upperCase, deburr, camelCase } from 'lodash';

export const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];

Expand All @@ -20,23 +20,44 @@ export function typeName(name: string): string {
return upperFirst(methodName(name));
}

/**
* Returns the name of the enum constant for a given value
*/
export function enumName(value: string, options: Options): string {
const name = toBasicChars(value, true);
if (options.enumModule === 'upper') {
return upperCase(name).replace(/\s+/g, '_');
} else {
return upperFirst(camelCase(name));
}
}

/**
* Returns a suitable method name for the given name
* @param name The raw name
*/
export function methodName(name: string) {
let result = camelCase(deburr(name));
if (/[0-9]/.test(result.charAt(0))) {
result = '_' + result;
}
return result;
return camelCase(toBasicChars(name, true));
}

/**
* Returns the file name for a given type name
*/
export function fileName(text: string): string {
return kebabCase(deburr(text));
return kebabCase(toBasicChars(text));
}

/**
* Converts a text to a basic, letters / numbers / underscore representation.
* When firstNonDigit is true, prepends the result with an uderscore if the first char is a digit.
*/
export function toBasicChars(text: string, firstNonDigit = false): string {
text = deburr((text || '').trim());
text = text.replace(/[^\w]+/g, '_');
if (firstNonDigit && /[0-9]/.test(text.charAt(0))) {
text = '_' + text;
}
return text;
}

/**
Expand Down
File renamed without changes.
File renamed without changes.
12 changes: 12 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env node
"use strict";
const tsNode = require('ts-node');
tsNode.register({
project: `${__dirname}/../tsconfig.json`,
transpileOnly: true
});

// Run the main function
const runNgOpenApiGen = require('./ng-openapi-gen').runNgOpenApiGen;
runNgOpenApiGen()
.catch(err => console.error(`Error on API generation: ${err}`));
21 changes: 16 additions & 5 deletions src/model.ts → lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GenType } from './gen-type';
import { fileName, modelClass, simpleName, tsComments, tsType } from './gen-utils';
import { Options } from './options';
import { Property } from './property';
import { EnumValue } from './enum-value';

/**
* Context to generate a model
Expand All @@ -11,11 +12,13 @@ export class Model extends GenType {

// General type
isSimple: boolean;
isArray: boolean;
isEnum: boolean;
isObject: boolean;

// Simple properties
simpleType: string;
enumValues: EnumValue[];
enumModule: boolean;

// Array properties
elementType: string;
Expand All @@ -36,23 +39,31 @@ export class Model extends GenType {
this.tsComments = tsComments(description, 0);

const type = schema.type || 'any';
this.isArray = type === 'array';

// When enumModule is 'alias' it is handled as a simple type.
if (options.enumModule !== 'alias' && (schema.enum || []).length > 0 && ['string', 'number', 'integer'].includes(type)) {
this.enumValues = (schema.enum || []).map(v => new EnumValue(type, v, options));
this.enumModule = true;
}

this.isObject = type === 'object' || (schema.allOf || []).length > 0;
this.isSimple = !this.isArray && !this.isObject;
this.isEnum = (this.enumValues || []).length > 0;
this.isSimple = !this.isObject && !this.isEnum;

if (this.isArray) {
if (type === 'array') {
// Array
this.elementType = tsType(schema.items, options);
} else if (this.isObject) {
// Object
this.superClasses = [];
const propertiesByName = new Map<string, Property>();
this.collectObject(schema, propertiesByName);
this.hasSuperClasses = this.superClasses.length > 0;
const sortedNames = [...propertiesByName.keys()];
sortedNames.sort();
this.properties = sortedNames.map(propName => propertiesByName.get(propName) as Property);
} else {
// Simple / union
// Simple / enum / union
this.simpleType = tsType(schema, options);
}
this.collectImports(schema);
Expand Down
Loading

0 comments on commit be08452

Please sign in to comment.