Skip to content

Commit

Permalink
Merge branch 'cyclosproject:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
alwyn974 authored Sep 1, 2023
2 parents b9f4f29 + 88e9be4 commit 5b756d3
Show file tree
Hide file tree
Showing 35 changed files with 86,536 additions and 75,074 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"args": [
"${workspaceFolder}/lib/index.ts",
"-i",
"${workspaceFolder}/test/cyclos.json",
"${workspaceFolder}/test/all-types.json",
"-o",
"${workspaceFolder}/out/cyclos",
"${workspaceFolder}/out/all-types",
"--fetch-timeout",
"2000"
]
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"[handlebars]": {
"editor.formatOnSave": false,
"editor.formatOnPaste": false,
"editor.suggest.insertMode": "replace"
"editor.suggest.insertMode": "replace",
"files.insertFinalNewline": false,
},
"files.insertFinalNewline": true,
"prettier.requireConfig": true
Expand Down
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ For a generator for [Swagger 2.0](https://github.com/OAI/OpenAPI-Specification/b
- 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`.
- Generated files should compile using strict `TypeScript` compiler flags, such as `noUnusedLocals` and `noUnusedParameters`;
- For large APIs it is possible to generate only functions for each API operation, and not entire services. This allows for tree-shakable code to be generated, resulting in lower bundle sizes.

## Limitations

Expand Down Expand Up @@ -59,7 +60,7 @@ $ npm install -g ng-openapi-gen
$ ng-openapi-gen --input my-api.yaml --output my-app/src/app/api
```

Alternativly you can use the generator directly from within your build-script:
Alternatively you can use the generator directly from within your build-script:

```typescript
import $RefParser from 'json-schema-ref-parser';
Expand Down Expand Up @@ -126,6 +127,55 @@ export class AppModule { }
Alternatively, you can inject the `ApiConfiguration` instance in some service
or component, such as the `AppComponent` and set the `rootUrl` property there.

## Using functional API calls

Starting with version 0.50.0, `ng-openapi-gen` generates a function with the implementation of each actual API call.
The generated services delegate to such functions.

However, it is possible to disable the entire services generation, which will avoid the need to include all such services in the application.
As a result, the application will be more tree-shakable, resulting in smaller bundle sizes.
This is specially true for large 3rd party APIs, in which, for example, a single service (OpenAPI tag) has many methods, but only a few are actually used.
Combined with the option `"enumStyle": "alias"`, the footprint of the API generation will be minimal.

Each generated function receives the following arguments:

- Angular's `HttpClient` instance;
- The API `rootUrl` (the operation knowns the relative URL, and will use this root URL to build the full endpoint path);
- The actual operation parameters. If it has no parameters or all parameters are optional, the params option will be optional as well;
- The optional http context.

Clients can directly call the function providing the given parameters.
However, to make the process smoother, it is also possible to generate a general service specifically to invoke such functions.
Its generation is disabled by default, but can be enabled by setting the option `"apiService": "ApiService"` (or another name your prefer).
With this, a single `@Injectable` service is generated. It will provide the functions with the `HttpClient` and `rootUrl` (from `ApiConfiguration`).

It then provides 2 methods for invoking the functions:

- `invoke`: Calls the function and returns the response body;
- `invoke$Response`: Calls the function and returns the entire response, so additional metadata can be read, such as status code or headers.

Here is an example class using the `ApiService`:

```typescript
import { Directive, OnInit, inject } from '@angular/core';
import { ApiService } from 'src/api/api.service';
import { getResults } from 'src/api/fn/api/get-results';
import { Result } from 'src/api/models';
import { Observable } from 'rxjs';

@Directive()
export class ApiFnComponent implements OnInit {
results$!: Observable<Result[]>;

apiService = inject(ApiService);

ngOnInit() {
// getResults is the operation function. The second argument is the actual parameters passed to the function
this.results$ = this.apiService.invoke(getResults, { limit: 10 });
}
}
```

## Passing request headers / customizing the request

To pass request headers, such as authorization or API keys, as well as having a
Expand Down
24 changes: 13 additions & 11 deletions lib/gen-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReferenceObject, SchemaObject } from 'openapi3-ts';
import { fileName, namespace, simpleName, typeName } from './gen-utils';
import { Importable } from './importable';
import { Import, Imports } from './imports';
import { Options } from './options';

Expand All @@ -23,6 +24,8 @@ export abstract class GenType {
/** TypeScript comments for this type */
tsComments: string;

pathToRoot: string;

imports: Import[];
private _imports: Imports;

Expand All @@ -32,7 +35,6 @@ export abstract class GenType {
constructor(
public name: string,
typeNameTransform: (typeName: string, options: Options) => string,
/** Generation options */
public options: Options) {

this.typeName = typeNameTransform(name, options);
Expand All @@ -46,17 +48,22 @@ export abstract class GenType {
this._imports = new Imports(options);
}

protected addImport(name: string) {
if (!this.skipImport(name)) {
// Don't have to import to this own file
this._imports.add(name, this.pathToModels());
protected addImport(param: string | Importable | null | undefined) {
if (param && !this.skipImport(param)) {
this._imports.add(param);
}
}

protected abstract skipImport(name: string): boolean;
protected abstract skipImport(name: string | Importable): boolean;

protected abstract initPathToRoot(): string;

protected updateImports() {
this.pathToRoot = this.initPathToRoot();
this.imports = this._imports.toArray();
for (const imp of this.imports) {
imp.path = this.pathToRoot + imp.path;
}
this.additionalDependencies = [...this._additionalDependencies];
}

Expand Down Expand Up @@ -93,9 +100,4 @@ export abstract class GenType {
}
}
}

/**
* Must be implemented to return the relative path to the models, ending with `/`
*/
protected abstract pathToModels(): string;
}
15 changes: 6 additions & 9 deletions lib/gen-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,16 @@ export function qualifiedName(name: string, options: Options): string {
}

/**
* Returns the file to import for a given model
* Returns the filename where to write a model with the given name
*/
export function modelFile(pathToModels: string, name: string, options: Options): string {
let dir = pathToModels || '';
if (dir.endsWith('/')) {
dir = dir.substr(0, dir.length - 1);
}
export function modelFile(name: string, options: Options): string {
let result = '';
const ns = namespace(name);
if (ns) {
dir += `/${ns}`;
result += `/${ns}`;
}
const file = unqualifiedName(name, options);
return dir += '/' + fileName(file);
return result += '/' + fileName(file);
}

/**
Expand Down Expand Up @@ -108,7 +105,7 @@ export function fileName(text: string): string {
*/
export function toBasicChars(text: string, firstNonDigit = false): string {
text = deburr((text || '').trim());
text = text.replace(/[^\w]+/g, '_');
text = text.replace(/[^\w$]+/g, '_');
if (firstNonDigit && /[0-9]/.test(text.charAt(0))) {
text = '_' + text;
}
Expand Down
9 changes: 9 additions & 0 deletions lib/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class Globals {
configurationParams: string;
baseServiceClass: string;
baseServiceFile: string;
apiServiceClass?: string;
apiServiceFile?: string;
requestBuilderClass: string;
requestBuilderFile: string;
responseClass: string;
Expand All @@ -27,6 +29,13 @@ export class Globals {
this.configurationParams = `${this.configurationClass}Params`;
this.baseServiceClass = options.baseService || 'BaseService';
this.baseServiceFile = fileName(this.baseServiceClass);
this.apiServiceClass = options.apiService || '';
if (this.apiServiceClass === '') {
this.apiServiceClass = undefined;
} else {
// Angular's best practices demands xxx.service.ts, not xxx-service.ts
this.apiServiceFile = fileName(this.apiServiceClass).replace(/\-service$/, '.service');
}
this.requestBuilderClass = options.requestBuilder || 'RequestBuilder';
this.requestBuilderFile = fileName(this.requestBuilderClass);
this.responseClass = options.response || 'StrictHttpResponse';
Expand Down
10 changes: 10 additions & 0 deletions lib/importable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* An artifact that can be imported
*/
export interface Importable {
importName: string;
importPath: string;
importFile: string;
importTypeName?: string;
importQualifiedName?: string;
}
54 changes: 43 additions & 11 deletions lib/imports.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
import { unqualifiedName, qualifiedName, modelFile } from './gen-utils';
import { modelFile, qualifiedName, unqualifiedName } from './gen-utils';
import { Importable } from './importable';
import { Options } from './options';

export class Import {
/** A general import */
export class Import implements Importable {
name: string;
typeName: string;
qualifiedName: string;
path: string;
file: string;
useAlias: boolean;
constructor(name: string, pathToModels: string, options: Options) {
fullPath: string;

// Fields from Importable
importName: string;
importPath: string;
importFile: string;
importTypeName?: string;
importQualifiedName?: string;

constructor(name: string, typeName: string, qName: string, path: string, file: string) {
this.name = name;
this.typeName = unqualifiedName(name, options);
this.qualifiedName = qualifiedName(name, options);
this.typeName = typeName;
this.qualifiedName = qName;
this.useAlias = this.typeName !== this.qualifiedName;
this.file = modelFile(pathToModels, name, options);
this.path = path;
this.file = file;
this.fullPath = `${this.path.split('/').filter(p => p.length).join('/')}/${this.file.split('/').filter(p => p.length).join('/')}`;

this.importName = name;
this.importPath = path;
this.importFile = file;
this.importTypeName = typeName;
this.importQualifiedName = qName;
}
}

Expand All @@ -28,13 +48,25 @@ export class Imports {
/**
* Adds an import
*/
add(name: string, pathToModels: string) {
this._imports.set(name, new Import(name, pathToModels, this.options));
add(param: string | Importable) {
let imp: Import;
if (typeof param === 'string') {
// A model
imp = new Import(param, unqualifiedName(param, this.options), qualifiedName(param, this.options), 'models/', modelFile(param, this.options));
} else {
// An Importable
imp = new Import(param.importName, param.importTypeName ?? param.importName, param.importQualifiedName ?? param.importName, `${param.importPath}`, param.importFile);
}
this._imports.set(imp.name, imp);
}

toArray(): Import[] {
const keys = [...this._imports.keys()];
keys.sort();
return keys.map(k => this._imports.get(k) as Import);
const array = [...this._imports.values()];
array.sort((a, b) => a.importName.localeCompare(b.importName));
return array;
}

get size() {
return this._imports.size;
}
}
23 changes: 23 additions & 0 deletions lib/model-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { GenType } from './gen-type';
import { Model } from './model';
import { Options } from './options';

/**
* Represents the model index
*/
export class ModelIndex extends GenType {

constructor(models: Model[], options: Options) {
super('models', n => n, options);
models.forEach(model => this.addImport(model.name));
this.updateImports();
}

protected skipImport(): boolean {
return false;
}

protected initPathToRoot(): string {
return './';
}
}
17 changes: 7 additions & 10 deletions lib/model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SchemaObject, OpenAPIObject } from 'openapi3-ts';
import { OpenAPIObject, SchemaObject } from 'openapi3-ts';
import { EnumValue } from './enum-value';
import { GenType } from './gen-type';
import { tsComments, tsType, unqualifiedName } from './gen-utils';
Expand Down Expand Up @@ -68,16 +68,13 @@ export class Model extends GenType {
this.updateImports();
}

protected pathToModels(): string {
protected initPathToRoot(): string {
if (this.namespace) {
const depth = this.namespace.split('/').length;
let path = '';
for (let i = 0; i < depth; i++) {
path += '../';
}
return path;
// for each namespace level go one directory up
// plus the "models" directory
return this.namespace.split('/').map(() => '../').join('').concat('../');
}
return './';
return '../';
}

protected skipImport(name: string): boolean {
Expand All @@ -97,7 +94,7 @@ export class Model extends GenType {
const appendType = (type: string) => {
if (type.startsWith('null | ')) {
propTypes.add('null');
propTypes.add(type.substr('null | '.length));
propTypes.add(type.substring('null | '.length));
} else {
propTypes.add(type);
}
Expand Down
Loading

0 comments on commit 5b756d3

Please sign in to comment.