├── .gitignore ├── .jshintrc ├── test ├── const.config.json ├── enums.config.json ├── cmd-args-test-config.json ├── allOf-required.config.json ├── model-name-collision.config.json ├── openapi31-content.config.json ├── openapi31-nullable.config.json ├── openapi31-webhooks.config.json ├── useTempDir.config.json ├── openapi31-jsonschema.config.json ├── polymorphic.config.json ├── self-ref.config.json ├── skipJsonSuffix.config.json ├── default-success-response.config.json ├── discriminator-inheritance.config.json ├── promises.config.json ├── self-ref-allof.config.json ├── observables.config.json ├── functionIndex.config.json ├── petstore-3.0.config.json ├── petstore-3.1.config.json ├── noModule.config.json ├── duplicate-x-operation-name.config.json ├── templates │ ├── handlebars.js │ └── service.handlebars ├── cbor-duplicate-methods.config.json ├── person-place.config.json ├── all-types.config.json ├── camelize-model-names.config.json ├── templates.config.json ├── all-operations.config.json ├── noModule.json ├── useTempDir.json ├── camelize-model-names.json ├── skipJsonSuffix.json ├── const.spec.ts ├── templates.json ├── enums.json ├── const.json ├── noModule.spec.ts ├── useTempDir.spec.ts ├── self-ref.json ├── duplicate-x-operation-name.json ├── functionIndex.spec.ts ├── cmd-args.spec.ts ├── allOf-required.json ├── functionIndex.json ├── self-ref-allof.json ├── default-success-response.json ├── duplicate-x-operation-name.spec.ts ├── camelize-model-names.spec.ts ├── skipJsonSuffix.spec.ts ├── parameter.spec.ts ├── templates.spec.ts ├── default-success-response.spec.ts ├── promises.spec.ts ├── observables.spec.ts ├── model-name-collision.json ├── model-name-collision.spec.ts ├── allOf-required.spec.ts ├── person-place.json ├── mock │ └── lib.ts ├── self-ref.spec.ts ├── polymorphic.json ├── discriminator-inheritance.spec.ts ├── discriminator-inheritance.json ├── cbor-duplicate-methods.json ├── enums.spec.ts ├── polymorphic.spec.ts ├── cbor-duplicate-methods.spec.ts ├── openapi31-nullable.json ├── openapi31-jsonschema.json ├── petstore-3.0.json ├── petstore-3.1.json ├── petstore-3.0.spec.ts ├── petstore-3.1.spec.ts ├── person-place.spec.ts ├── openapi31-webhooks.spec.ts ├── openapi31-nullable.spec.ts ├── openapi31-webhooks.json └── openapi31-jsonschema.spec.ts ├── templates ├── operationPath.handlebars ├── enum.handlebars ├── operationParameters.handlebars ├── serviceIndex.handlebars ├── object.handlebars ├── modelIndex.handlebars ├── functionIndex.handlebars ├── enumArray.handlebars ├── response.handlebars ├── simple.handlebars ├── model.handlebars ├── handleResponse.handlebars ├── operationResponse.handlebars ├── operationBody.handlebars ├── configuration.handlebars ├── baseService.handlebars ├── index.handlebars ├── service.handlebars ├── module.handlebars ├── fn.handlebars └── apiService.handlebars ├── lib ├── index.ts ├── importable.ts ├── response.ts ├── content.ts ├── model-index.ts ├── request-body.ts ├── logger.ts ├── enum-value.ts ├── handlebars-manager.ts ├── security.ts ├── property.ts ├── parameter.ts ├── service.ts ├── templates.ts ├── globals.ts ├── imports.ts ├── gen-type.ts ├── openapi-typings.ts ├── cmd-args.ts ├── operation-variant.ts └── options.ts ├── tsconfig.build.json ├── vitest.config.ts ├── .github └── workflows │ └── build.yml ├── tsconfig.json ├── .vscode ├── settings.json └── launch.json ├── scripts └── prepare-dist-package.js ├── LICENSE ├── package.json └── eslint.config.mjs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .directory 3 | dist 4 | out 5 | .idea 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": false, 3 | "esversion": 6, 4 | "node": true 5 | } -------------------------------------------------------------------------------- /test/const.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "enums.json" 4 | } 5 | -------------------------------------------------------------------------------- /test/enums.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "enums.json" 4 | } 5 | -------------------------------------------------------------------------------- /templates/operationPath.handlebars: -------------------------------------------------------------------------------- 1 | /** Path part for operation `{{id}}()` */ 2 | static readonly {{pathVar}} = '{{{path}}}'; 3 | -------------------------------------------------------------------------------- /test/cmd-args-test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "customizedResponseType": { 3 | "abc": { 4 | "toUse": "arraybuffer" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /templates/enum.handlebars: -------------------------------------------------------------------------------- 1 | export enum {{{typeName}}} { 2 | {{#enumValues}} {{{name}}} = {{{value}}}{{^@last}}, 3 | {{/@last}}{{/enumValues}} 4 | } -------------------------------------------------------------------------------- /templates/operationParameters.handlebars: -------------------------------------------------------------------------------- 1 | params{{^operation.parametersRequired}}?{{/operation.parametersRequired}}: {{paramsType}}, context?: HttpContext -------------------------------------------------------------------------------- /test/allOf-required.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "allOf-required.json", 4 | "output": "out/allOf-required" 5 | } 6 | -------------------------------------------------------------------------------- /test/model-name-collision.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": "test/model-name-collision.json", 3 | "output": "out/model-name-collision", 4 | "ignoreUnusedModels": false 5 | } 6 | -------------------------------------------------------------------------------- /test/openapi31-content.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/openapi31-content.json", 4 | "output": "out/openapi31-content" 5 | } 6 | -------------------------------------------------------------------------------- /test/openapi31-nullable.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/openapi31-nullable.json", 4 | "output": "out/openapi31-nullable" 5 | } 6 | -------------------------------------------------------------------------------- /test/openapi31-webhooks.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/openapi31-webhooks.json", 4 | "output": "out/openapi31-webhooks" 5 | } 6 | -------------------------------------------------------------------------------- /test/useTempDir.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/useTempDir.json", 4 | "output": "out/useTempDir", 5 | "useTempDir": true 6 | } 7 | -------------------------------------------------------------------------------- /test/openapi31-jsonschema.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/openapi31-jsonschema.json", 4 | "output": "out/openapi31-jsonschema" 5 | } 6 | -------------------------------------------------------------------------------- /test/polymorphic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "polymorphic.json", 4 | "output": "out/polymorphic", 5 | "ignoreUnusedModels": false 6 | } -------------------------------------------------------------------------------- /test/self-ref.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "self-ref-array.json", 4 | "output": "out/self-ref-array", 5 | "ignoreUnusedModels": false 6 | } -------------------------------------------------------------------------------- /test/skipJsonSuffix.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "skipJsonSuffix.json", 4 | "output": "out/skipJsonSuffix", 5 | "skipJsonSuffix": true 6 | } -------------------------------------------------------------------------------- /test/default-success-response.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "default-success-response.json", 4 | "output": "out/default-success-response" 5 | } 6 | -------------------------------------------------------------------------------- /test/discriminator-inheritance.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "discriminator-inheritance.json", 4 | "output": "out/discriminator-inheritance" 5 | } 6 | -------------------------------------------------------------------------------- /test/promises.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "petstore-3.0.json", 4 | "output": "out/promises", 5 | "services": true, 6 | "promises": true 7 | } 8 | -------------------------------------------------------------------------------- /test/self-ref-allof.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "self-ref-allof.json", 4 | "output": "out/self-ref-allof", 5 | "ignoreUnusedModels": false 6 | } 7 | -------------------------------------------------------------------------------- /templates/serviceIndex.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | {{#services}}export { {{typeName}} } from './services/{{{fileName}}}'; 5 | {{/services}} 6 | -------------------------------------------------------------------------------- /test/observables.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "petstore-3.0.json", 4 | "output": "out/observables", 5 | "services": true, 6 | "promises": false 7 | } 8 | -------------------------------------------------------------------------------- /test/functionIndex.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/functionIndex.json", 4 | "output": "out/functionIndex", 5 | "useTempDir": true, 6 | "functionIndex": true 7 | } 8 | -------------------------------------------------------------------------------- /test/petstore-3.0.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "petstore-3.0.json", 4 | "output": "out/petstore-3.0", 5 | "modelPrefix": "Petstore", 6 | "modelSuffix": "Model" 7 | } 8 | -------------------------------------------------------------------------------- /test/petstore-3.1.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "petstore-3.1.json", 4 | "output": "out/petstore-3.1", 5 | "modelPrefix": "Petstore", 6 | "modelSuffix": "Model" 7 | } 8 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { runNgOpenApiGen } from './ng-openapi-gen'; 4 | 5 | // Run the main function 6 | runNgOpenApiGen() 7 | .catch(err => console.error(`Error on API generation: ${err}`)); 8 | 9 | -------------------------------------------------------------------------------- /test/noModule.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/noModule.json", 4 | "output": "out/noModule", 5 | "useTempDir": true, 6 | "module": false, 7 | "indexFile": true 8 | } 9 | -------------------------------------------------------------------------------- /test/duplicate-x-operation-name.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/duplicate-x-operation-name.json", 4 | "output": "out/duplicate-x-operation-name", 5 | "indexFile": true 6 | } 7 | -------------------------------------------------------------------------------- /test/templates/handlebars.js: -------------------------------------------------------------------------------- 1 | module.exports = function(handlebars) { 2 | // Adding a custom handlebars helper: loud 3 | handlebars.registerHelper('loud', function (aString) { 4 | return aString.toUpperCase(); 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /test/cbor-duplicate-methods.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "cbor-duplicate-methods.json", 4 | "output": "out/cbor-duplicate-methods", 5 | "services": true, 6 | "promises": false 7 | } 8 | -------------------------------------------------------------------------------- /test/person-place.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "person-place.json", 4 | "output": "out/person-place", 5 | "ignoreUnusedModels": false, 6 | "modelPrefix": "PP", 7 | "modelSuffix": "Model" 8 | } 9 | -------------------------------------------------------------------------------- /lib/importable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An artifact that can be imported 3 | */ 4 | export interface Importable { 5 | importName: string; 6 | importPath: string; 7 | importFile: string; 8 | importTypeName?: string; 9 | importQualifiedName?: string; 10 | } 11 | -------------------------------------------------------------------------------- /test/all-types.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "all-types.json", 4 | "output": "out/all-types/", 5 | "indexFile": true, 6 | "enumStyle": "pascal", 7 | "module": "AllTypesModule", 8 | "services": true 9 | } 10 | -------------------------------------------------------------------------------- /templates/object.handlebars: -------------------------------------------------------------------------------- 1 | export interface {{typeName}} { 2 | {{#properties}} 3 | {{{tsComments}}}{{{identifier}}}{{^required}}?{{/required}}: {{{type}}}; 4 | {{/properties}} 5 | {{#additionalPropertiesType}} 6 | 7 | [key: string]: {{{.}}}; 8 | {{/additionalPropertiesType}} 9 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "skipLibCheck": true 5 | }, 6 | "exclude": [ 7 | "test/**/*", 8 | "vitest.config.ts", 9 | "node_modules", 10 | "dist" 11 | ], 12 | "include": [ 13 | "lib/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /test/camelize-model-names.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "keep-model-names.json", 4 | "output": "out/keep-model-names/", 5 | "camelizeModelNames": false, 6 | "ignoreUnusedModels": false, 7 | "modelPrefix": "Pre", 8 | "modelSuffix": "Pos" 9 | } 10 | -------------------------------------------------------------------------------- /test/templates.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "test/templates.json", 4 | "output": "out/templates", 5 | "defaultTag": "noTag", 6 | "templates": "test/templates", 7 | "services": true, 8 | "excludeParameters": [ 9 | "X-Exclude" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /templates/modelIndex.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | {{#modelIndex.imports}}export {{#typeOnly}}type {{/typeOnly}}{ {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from '{{{@root.modelIndex.pathToRoot}}}{{{fullPath}}}'; 5 | {{/modelIndex.imports}} 6 | -------------------------------------------------------------------------------- /templates/functionIndex.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | {{#functions}}export type { {{paramsType}} as {{paramsTypeExportName}} } from './{{{importPath}}}/{{{importFile}}}'; 5 | export { {{importName}} as {{exportName}} } from './{{{importPath}}}/{{{importFile}}}'; 6 | {{/functions}} 7 | -------------------------------------------------------------------------------- /lib/response.ts: -------------------------------------------------------------------------------- 1 | import { Content } from './content'; 2 | import { Options } from './options'; 3 | 4 | /** 5 | * An operation response 6 | */ 7 | export class Response { 8 | constructor( 9 | public statusCode: string, 10 | public description: string, 11 | public content: Content[], 12 | public options: Options) { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'node', 7 | include: ['test/**/*.spec.ts'], 8 | testTimeout: 10000, 9 | reporters: ['verbose'] 10 | }, 11 | resolve: { 12 | alias: { 13 | '@': './lib' 14 | } 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /templates/enumArray.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { {{{typeName}}} } from './{{{fileName}}}'; 5 | 6 | /** 7 | * Each possible value of `{{{typeName}}}` 8 | */ 9 | export const {{{enumArrayName}}}: {{{typeName}}}[] = [ 10 | {{#enumValues}} {{{value}}}{{^@last}}, 11 | {{/@last}}{{/enumValues}} 12 | ]; -------------------------------------------------------------------------------- /templates/response.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { HttpResponse } from '@angular/common/http'; 5 | 6 | /** 7 | * Constrains the http response to not have the body defined as `T | null`, but `T` only. 8 | */ 9 | export type {{ responseClass }} = HttpResponse & { 10 | readonly body: T; 11 | } 12 | -------------------------------------------------------------------------------- /templates/simple.handlebars: -------------------------------------------------------------------------------- 1 | {{#if orphanRequiredProperties}} 2 | type {{typeName}}$ = {{{simpleType}}}; 3 | type RequiredProperties = {{#each orphanRequiredProperties}}'{{this}}'{{#unless @last}} | {{/unless}}{{/each}}; 4 | export type {{typeName}} = {{typeName}}$ & Required>; 5 | {{else}} 6 | export type {{typeName}} = {{{simpleType}}}; 7 | {{/if}} 8 | -------------------------------------------------------------------------------- /templates/model.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | {{#imports}}import { {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from '{{{@root.pathToRoot}}}{{{fullPath}}}'; 5 | {{/imports}} 6 | {{{tsComments}}}{{#isObject}}{{>object}}{{/isObject 7 | }}{{#isEnum}}{{>enum}}{{/isEnum 8 | }}{{#isSimple}}{{>simple}}{{/isSimple 9 | }} 10 | -------------------------------------------------------------------------------- /templates/handleResponse.handlebars: -------------------------------------------------------------------------------- 1 | {{#isVoid}}return (r as HttpResponse).clone({ body: undefined }){{/isVoid 2 | }}{{#isNumber}}return (r as HttpResponse).clone({ body: parseFloat(String((r as HttpResponse).body)) }){{/isNumber 3 | }}{{#isBoolean}}return (r as HttpResponse).clone({ body: String((r as HttpResponse).body) === 'true' }){{/isBoolean 4 | }}{{#isOther}}return r{{/isOther}} as {{@root.responseClass}}<{{{resultType}}}>; 5 | -------------------------------------------------------------------------------- /templates/operationResponse.handlebars: -------------------------------------------------------------------------------- 1 | {{{responseMethodTsComments}}}{{responseMethodName}}({{>operationParameters}}): {{#if @root.promises}}Promise<{{@root.responseClass}}<{{{resultType}}}>>{{else}}Observable<{{@root.responseClass}}<{{{resultType}}}>>{{/if}} { 2 | const obs = {{importName}}(this.http, this.rootUrl, params, context); 3 | {{#if @root.promises}} 4 | return firstValueFrom(obs); 5 | {{else}} 6 | return obs; 7 | {{/if}} 8 | } 9 | -------------------------------------------------------------------------------- /test/all-operations.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../ng-openapi-gen-schema.json", 3 | "input": "all-operations.json", 4 | "output": "out/all-operations", 5 | "defaultTag": "noTag", 6 | "indexFile": true, 7 | "apiService": false, 8 | "excludeParameters": [ 9 | "X-Exclude" 10 | ], 11 | "keepFullResponseMediaType": [ 12 | { 13 | "mediaType": "spring", 14 | "use": "full" 15 | }, 16 | { 17 | "mediaType": "hal\\+json", 18 | "use": "tail" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /lib/content.ts: -------------------------------------------------------------------------------- 1 | import { tsType } from './gen-utils'; 2 | import { MediaTypeObject, OpenAPIObject } from './openapi-typings'; 3 | import { Options } from './options'; 4 | 5 | /** 6 | * Either a request body or response content 7 | */ 8 | export class Content { 9 | type: string; 10 | 11 | constructor( 12 | public mediaType: string, 13 | public spec: MediaTypeObject, 14 | public options: Options, 15 | public openApi: OpenAPIObject) { 16 | this.type = tsType(spec.schema, options, openApi); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 22.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 22.x 18 | - run: npm install 19 | - run: cat package.json 20 | - run: node --version 21 | - run: ls -l test/ 22 | - run: ls -l lib/ 23 | - run: npm run build 24 | -------------------------------------------------------------------------------- /test/noModule.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test with noModule", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/foo": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "type": "string" 16 | } 17 | }, 18 | "text/plain": {} 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/useTempDir.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test with useTempDir", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/foo": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "type": "string" 16 | } 17 | }, 18 | "text/plain": {} 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /templates/operationBody.handlebars: -------------------------------------------------------------------------------- 1 | {{{bodyMethodTsComments}}}{{methodName}}({{>operationParameters}}): {{#if @root.promises}}Promise<{{{resultType}}}>{{else}}Observable<{{{resultType}}}>{{/if}} { 2 | const resp = this.{{responseMethodName}}(params, context); 3 | {{#if @root.promises}} 4 | return resp.then((r: {{@root.responseClass}}<{{{resultType}}}>): {{{resultType}}} => r.body); 5 | {{else}} 6 | return resp.pipe( 7 | map((r: {{@root.responseClass}}<{{{resultType}}}>): {{{resultType}}} => r.body) 8 | ); 9 | {{/if}} 10 | } 11 | -------------------------------------------------------------------------------- /lib/model-index.ts: -------------------------------------------------------------------------------- 1 | import { GenType } from './gen-type'; 2 | import { Model } from './model'; 3 | import { Options } from './options'; 4 | 5 | /** 6 | * Represents the model index 7 | */ 8 | export class ModelIndex extends GenType { 9 | 10 | constructor(models: Model[], options: Options) { 11 | super('models', n => n, options); 12 | models.forEach(model => this.addImport(model.name, !model.isEnum)); 13 | this.updateImports(); 14 | } 15 | 16 | protected skipImport(): boolean { 17 | return false; 18 | } 19 | 20 | protected initPathToRoot(): string { 21 | return './'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/request-body.ts: -------------------------------------------------------------------------------- 1 | import { Content } from './content'; 2 | import { tsComments } from './gen-utils'; 3 | import { RequestBodyObject } from './openapi-typings'; 4 | import { Options } from './options'; 5 | 6 | /** 7 | * Describes a request body 8 | */ 9 | export class RequestBody { 10 | 11 | tsComments: string; 12 | required: boolean; 13 | 14 | constructor( 15 | public spec: RequestBodyObject, 16 | public content: Content[], 17 | public options: Options) { 18 | 19 | this.tsComments = tsComments(spec.description, 2); 20 | this.required = spec.required === true; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /test/camelize-model-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test for keepModelNames", 5 | "version": "1.0" 6 | }, 7 | "paths": [], 8 | "components": { 9 | "schemas": { 10 | "snake-case": { 11 | "type": "object", 12 | "properties": { 13 | "name": { 14 | "type": "string" 15 | } 16 | } 17 | }, 18 | "camelCase": { 19 | "type": "object", 20 | "properties": { 21 | "name": { 22 | "type": "string" 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/skipJsonSuffix.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test with skipJsonSuffix", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/foo": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "type": "string" 16 | } 17 | }, 18 | "text/plain": {} 19 | } 20 | } 21 | } 22 | } 23 | }, 24 | "/bar": { 25 | "get": { 26 | "responses": { 27 | "200": { 28 | "content": { 29 | "text/plain": {} 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /lib/logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | constructor(public silent = false) { } 3 | 4 | info(message: string, ...optionalParams: []) { 5 | if (!this.silent) { 6 | console.info(message, ...optionalParams); 7 | } 8 | } 9 | 10 | log(message: string, ...optionalParams: []) { 11 | if (!this.silent) { 12 | console.log(message, ...optionalParams); 13 | } 14 | } 15 | 16 | debug(message: string, ...optionalParams: []) { 17 | if (!this.silent) { 18 | console.debug(message, ...optionalParams); 19 | } 20 | } 21 | 22 | warn(message: string, ...optionalParams: []) { 23 | console.warn(message, ...optionalParams); 24 | } 25 | 26 | error(message: string, ...optionalParams: []) { 27 | console.error(message, ...optionalParams); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "esModuleInterop": true, 5 | "target": "ES2020", 6 | "noImplicitAny": true, 7 | "moduleResolution": "node", 8 | "sourceMap": true, 9 | "strictNullChecks": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "resolveJsonModule": true, 13 | "outDir": "dist", 14 | "baseUrl": ".", 15 | "declaration": true, 16 | "paths": { 17 | "*": [ 18 | "node_modules/*", 19 | "lib/types/*" 20 | ] 21 | }, 22 | "types": [ 23 | "node", 24 | "vitest/globals" 25 | ], 26 | "lib": [ 27 | "es2017", 28 | "dom" 29 | ] 30 | }, 31 | "include": [ 32 | "lib/**/*", 33 | "test/**/*", 34 | "vitest.config.ts" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /test/const.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './const.config.json'; 3 | import constsSpec from './const.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = constsSpec as unknown as OpenAPIObject; 9 | 10 | describe('Test const generation', () => { 11 | 12 | it('const', () => { 13 | const genDefault = new NgOpenApiGen(spec, { 14 | ...options, 15 | output: 'out/constStyle/default/' 16 | } as Options); 17 | genDefault.generate(); 18 | const fileContents = fs.readFileSync(fs.realpathSync(`${genDefault.outDir}/models/flavor-const.ts`)); 19 | expect(/flavor: 'IonlyWantChocolate'/.test(fileContents.toString())).toBe(true); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [140], 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": false, 5 | "editor.tabSize": 2, 6 | "typescript.preferences.quoteStyle": "single", 7 | "typescript.preferences.importModuleSpecifier": "relative", 8 | "typescript.tsdk": "node_modules/typescript/lib", 9 | "[typescript]": { 10 | "editor.formatOnSave": true 11 | }, 12 | "[json]": { 13 | "editor.formatOnSave": true 14 | }, 15 | "[handlebars]": { 16 | "editor.formatOnSave": false, 17 | "editor.formatOnPaste": false, 18 | "editor.suggest.insertMode": "replace", 19 | "files.insertFinalNewline": false, 20 | }, 21 | "files.insertFinalNewline": true, 22 | "prettier.requireConfig": true, 23 | "debug.javascript.breakOnConditionalError": false, 24 | "debug.javascript.pauseForSourceMap": false 25 | } 26 | -------------------------------------------------------------------------------- /lib/enum-value.ts: -------------------------------------------------------------------------------- 1 | import jsesc from 'jsesc'; 2 | import { enumName } from './gen-utils'; 3 | import { Options } from './options'; 4 | 5 | /** 6 | * Represents a possible enumerated value 7 | */ 8 | export class EnumValue { 9 | name: string; 10 | value: string; 11 | description: string; 12 | 13 | constructor(public type: string, name: string | undefined, description: string | undefined, _value: any, public options: Options) { 14 | const value = String(_value); 15 | this.name = name || enumName(value, options); 16 | this.description = description || this.name; 17 | if (this.name === '') { 18 | this.name = '_'; 19 | } 20 | if (this.description === '') { 21 | this.description = this.name; 22 | } 23 | if (type === 'string') { 24 | this.value = `'${jsesc(value)}'`; 25 | } else { 26 | this.value = value; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /templates/configuration.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { Injectable } from '@angular/core'; 5 | 6 | /** 7 | * Provides the {{configurationClass}} configuration object with a given root URL. 8 | */ 9 | export function provide{{configurationClass}}(rootUrl: string) { 10 | var config = new {{configurationClass}}(); 11 | config.rootUrl = rootUrl; 12 | return { 13 | provide: {{configurationClass}}, 14 | useValue: config 15 | }; 16 | } 17 | 18 | /** 19 | * Global configuration 20 | */ 21 | @Injectable({ 22 | providedIn: 'root', 23 | }) 24 | export class {{configurationClass}} { 25 | rootUrl: string = '{{{rootUrl}}}'; 26 | } 27 | 28 | {{#if moduleClass}} 29 | /** 30 | * Parameters for `{{moduleClass}}.forRoot()` 31 | */ 32 | export interface {{configurationParams}} { 33 | rootUrl?: string; 34 | } 35 | {{/if}} 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run tests", 8 | "program": "${workspaceFolder}/node_modules/.bin/vitest", 9 | "args": [ 10 | "run" 11 | ], 12 | "console": "integratedTerminal", 13 | "cwd": "${workspaceFolder}", 14 | "stopOnEntry": false, 15 | "skipFiles": [ 16 | "/**", 17 | "node_modules/**" 18 | ] 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Run directly", 24 | "program": "${workspaceFolder}/dist/lib/index.js", 25 | "args": [ 26 | "-i", 27 | "${workspaceFolder}/test/all-types.json", 28 | "-o", 29 | "${workspaceFolder}/out/all-types", 30 | "--fetch-timeout", 31 | "2000" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /test/templates.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test with only one operation that uses custom handlebars helpers", 5 | "version": "1.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "/api/{version}", 10 | "variables": { 11 | "version": { 12 | "default": "1.0" 13 | } 14 | } 15 | } 16 | ], 17 | "tags": [ 18 | { 19 | "name": "tag1", 20 | "description": "Description of tag1" 21 | } 22 | ], 23 | "paths": { 24 | "/path1": { 25 | "parameters": [], 26 | "get": { 27 | "operationId": "path1Get", 28 | "tags": [ 29 | "tag1" 30 | ], 31 | "description": "Path 1 GET description", 32 | "parameters": [], 33 | "responses": { 34 | "201": { 35 | "content": { 36 | "application/json": {} 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /templates/baseService.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { Injectable } from '@angular/core'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { {{configurationClass}} } from './{{configurationFile}}'; 7 | 8 | /** 9 | * Base class for services 10 | */ 11 | @Injectable() 12 | export class {{baseServiceClass}} { 13 | constructor( 14 | protected config: {{configurationClass}}, 15 | protected http: HttpClient 16 | ) { 17 | } 18 | 19 | private _rootUrl?: string; 20 | 21 | /** 22 | * Returns the root url for all operations in this service. If not set directly in this 23 | * service, will fallback to `{{configurationClass}}.rootUrl`. 24 | */ 25 | get rootUrl(): string { 26 | return this._rootUrl || this.config.rootUrl; 27 | } 28 | 29 | /** 30 | * Sets the root URL for API operations in this service. 31 | */ 32 | set rootUrl(rootUrl: string) { 33 | this._rootUrl = rootUrl; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /scripts/prepare-dist-package.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | // Read the source package.json 7 | const sourcePackage = JSON.parse(fs.readFileSync('package.json', 'utf8')); 8 | 9 | // Create a clean package.json for distribution 10 | const distPackage = { 11 | name: sourcePackage.name, 12 | version: sourcePackage.version, 13 | license: sourcePackage.license, 14 | author: sourcePackage.author, 15 | description: sourcePackage.description, 16 | keywords: sourcePackage.keywords, 17 | repository: sourcePackage.repository, 18 | private: false, 19 | bin: sourcePackage.bin, 20 | main: sourcePackage.main, 21 | dependencies: sourcePackage.dependencies, 22 | peerDependencies: sourcePackage.peerDependencies 23 | }; 24 | 25 | // Write the clean package.json to dist 26 | fs.writeFileSync( 27 | path.join('dist', 'package.json'), 28 | JSON.stringify(distPackage, null, 2) + '\n' 29 | ); 30 | 31 | console.log('✅ Created clean dist/package.json'); 32 | -------------------------------------------------------------------------------- /test/enums.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test generation styles of enum", 5 | "version": "1.0" 6 | }, 7 | "components" : { 8 | "schemas" : { 9 | "FlavorEnum" : { 10 | "description" : "Some ice-cream flavors", 11 | "type" : "string", 12 | "enum" : [ 13 | "vanilla", 14 | "StrawBerry", 15 | "cookie dough", 16 | "Chocolate Chip", 17 | "butter_pecan", 18 | "COKE light" 19 | ] 20 | } 21 | } 22 | }, 23 | "paths": { 24 | "/foo": { 25 | "get": { 26 | "responses": { 27 | "200": { 28 | "content": { 29 | "application/json": { 30 | "schema": { 31 | "type" : "array", 32 | "items" : { 33 | "$ref" : "#/components/schemas/FlavorEnum" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/const.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test generation styles of const", 5 | "version": "1.0" 6 | }, 7 | "components" : { 8 | "schemas" : { 9 | "FlavorConst" : { 10 | "description" : "Some ice-cream flavors", 11 | "type" : "object", 12 | "required" : [ "flavor" ], 13 | "properties" : { 14 | "flavor" : { 15 | "type" : "string", 16 | "const" : "IonlyWantChocolate" 17 | } 18 | } 19 | } 20 | } 21 | }, 22 | "paths": { 23 | "/foo": { 24 | "get": { 25 | "responses": { 26 | "200": { 27 | "content": { 28 | "application/json": { 29 | "schema": { 30 | "type" : "array", 31 | "items" : { 32 | "$ref" : "#/components/schemas/FlavorConst" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/handlebars-manager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as Handlebars from 'handlebars'; 3 | import { Options } from './options'; 4 | 5 | /** 6 | * Handlebars manager 7 | */ 8 | export class HandlebarsManager { 9 | 10 | public instance: typeof Handlebars = Handlebars; 11 | 12 | public readCustomJsFile(options: Options): void { 13 | const customDir = options.templates || ''; 14 | 15 | // Attempt to find "handlebars.js" in template folder to allow for custom 16 | // Handlebars settings (ex: adding helpers) 17 | const handlerbarsJsFile = customDir ? `${customDir}/handlebars.js` : null; 18 | if (handlerbarsJsFile && fs.existsSync(handlerbarsJsFile)) { 19 | 20 | // Attempt to import the "handlebars.js" file if it exists 21 | const handlebarsFn = require(fs.realpathSync(handlerbarsJsFile)); 22 | if (handlebarsFn && typeof handlebarsFn === 'function') { 23 | // call imported method and pass the Handlebars instance to allow for helpers to be registered 24 | handlebarsFn.call(this.instance, this.instance); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /templates/index.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | export { {{configurationClass}} } from './{{{configurationFile}}}'; 5 | export { {{requestBuilderClass}} } from './{{{requestBuilderFile}}}'; 6 | export type { {{responseClass}} } from './{{{responseFile}}}'; 7 | {{#if apiServiceClass}}export { {{apiServiceClass}} } from './{{{apiServiceFile}}}';{{/if}} 8 | {{#if moduleClass}}export { {{moduleClass}} } from './{{{moduleFile}}}';{{/if}} 9 | {{#modelIndex.imports}}export {{#typeOnly}}type {{/typeOnly}}{ {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from './models{{{file}}}'; 10 | {{/modelIndex.imports}} 11 | {{#if generateServices}}export { {{baseServiceClass}} } from './{{{baseServiceFile}}}'; 12 | {{#services}}export { {{typeName}} } from './services/{{{fileName}}}'; 13 | {{/services}}{{/if}} 14 | {{#functions}}export type { {{paramsType}} as {{paramsTypeExportName}} } from './{{{importPath}}}/{{{importFile}}}'; 15 | export { {{importName}} as {{exportName}} } from './{{{importPath}}}/{{{importFile}}}'; 16 | {{/functions}} 17 | -------------------------------------------------------------------------------- /templates/service.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { HttpClient, HttpContext } from '@angular/common/http'; 5 | import { Injectable } from '@angular/core'; 6 | {{#if promises}} 7 | import { firstValueFrom } from 'rxjs'; 8 | {{else}} 9 | import { Observable } from 'rxjs'; 10 | import { map } from 'rxjs/operators'; 11 | {{/if}} 12 | 13 | import { {{baseServiceClass}} } from '../{{baseServiceFile}}'; 14 | import { {{configurationClass}} } from '../{{configurationFile}}'; 15 | import { {{responseClass}} } from '../{{responseFile}}'; 16 | 17 | {{#imports}}import { {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from '{{{@root.pathToRoot}}}{{{fullPath}}}'; 18 | {{/imports}} 19 | 20 | {{{tsComments}}}@Injectable({ providedIn: 'root' }) 21 | export class {{typeName}} extends {{baseServiceClass}} { 22 | constructor(config: {{configurationClass}}, http: HttpClient) { 23 | super(config, http); 24 | } 25 | 26 | {{#operations}} 27 | {{>operationPath}} 28 | {{#variants}}{{>operationResponse}}{{>operationBody}}{{/variants}} 29 | {{/operations}} 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017 Social Trade Organisation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/templates/service.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { Injectable } from '@angular/core'; 3 | import { HttpClient, HttpResponse, HttpContext } from '@angular/common/http'; 4 | import { {{baseServiceClass}} } from '../{{baseServiceFile}}'; 5 | import { {{configurationClass}} } from '../{{configurationFile}}'; 6 | import { {{responseClass}} } from '../{{responseFile}}'; 7 | import { {{requestBuilderClass}} } from '../{{requestBuilderFile}}'; 8 | import { Observable } from 'rxjs'; 9 | import { map } from 'rxjs/operators/map'; 10 | import { filter } from 'rxjs/operators/filter'; 11 | 12 | {{#imports}}import { {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from '{{{@root.pathToRoot}}}{{{fullPath}}}'; 13 | {{/imports}} 14 | 15 | {{{tsComments}}}@Injectable({ 16 | providedIn: 'root', 17 | }) 18 | export class {{typeName}} extends {{baseServiceClass}} { 19 | constructor( 20 | config: {{configurationClass}}, 21 | http: HttpClient 22 | ) { 23 | super(config, http); 24 | } 25 | 26 | {{#operations}} 27 | {{>operationPath}} 28 | {{#variants}}{{>operationResponse}}{{>operationBody}}{{/variants}} 29 | {{/operations}} 30 | } 31 | -------------------------------------------------------------------------------- /lib/security.ts: -------------------------------------------------------------------------------- 1 | import { tsComments, methodName } from './gen-utils'; 2 | import { SecuritySchemeObject, ApiKeySecurityScheme } from './openapi-typings'; 3 | 4 | /** 5 | * An operation security 6 | */ 7 | export class Security { 8 | /** 9 | * variable name 10 | */ 11 | var: string; 12 | 13 | /** 14 | * Header Name 15 | */ 16 | name: string; 17 | 18 | /** 19 | * Property Description 20 | */ 21 | tsComments: string; 22 | 23 | /** 24 | * Location of security parameter 25 | */ 26 | in: string; 27 | type: string; 28 | 29 | constructor(key: string, public spec: SecuritySchemeObject, public scope: string[] = []) { 30 | // Handle different types of security schemes 31 | if (spec.type === 'apiKey') { 32 | const apiKeySpec = spec as ApiKeySecurityScheme; 33 | this.name = apiKeySpec.name || ''; 34 | this.in = apiKeySpec.in || 'header'; 35 | } else { 36 | this.name = ''; 37 | this.in = 'header'; 38 | } 39 | 40 | this.var = methodName(key); 41 | this.tsComments = tsComments(spec.description || '', 2); 42 | this.type = 'string'; // Default type for security parameters 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/property.ts: -------------------------------------------------------------------------------- 1 | import { escapeId, tsComments, tsType } from './gen-utils'; 2 | import { Model } from './model'; 3 | import { OpenAPIObject, ReferenceObject, SchemaObject } from './openapi-typings'; 4 | import { Options } from './options'; 5 | 6 | /** 7 | * An object property 8 | */ 9 | export class Property { 10 | 11 | identifier: string; 12 | tsComments: string; 13 | type: string; 14 | 15 | constructor( 16 | public model: Model, 17 | public name: string, 18 | public schema: SchemaObject | ReferenceObject, 19 | public required: boolean, 20 | public options: Options, 21 | public openApi: OpenAPIObject) { 22 | 23 | // Defer type resolution until after imports are finalized 24 | this.type = ''; // Will be set later 25 | this.identifier = escapeId(this.name); 26 | const description = (schema as SchemaObject).description || ''; 27 | this.tsComments = tsComments(description, 1, (schema as SchemaObject).deprecated); 28 | } 29 | 30 | /** 31 | * Resolves the property type after imports are finalized 32 | */ 33 | resolveType(): void { 34 | this.type = tsType(this.schema, this.options, this.openApi, this.model); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/noModule.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import { OpenAPIObject } from '../lib/openapi-typings'; 3 | import options from './noModule.config.json'; 4 | import templatesSpec from './noModule.json'; 5 | import { NamedExport, TypescriptParser } from 'typescript-parser'; 6 | 7 | const spec = templatesSpec as unknown as OpenAPIObject; 8 | 9 | describe('Generation tests with index and no ApiModule', () => { 10 | const gen = new NgOpenApiGen(spec, options); 11 | 12 | beforeAll(() => { 13 | gen.generate(); 14 | }); 15 | 16 | it('index file', () => { 17 | const ref = gen.models.get('InlineObject'); 18 | const ts = gen.templates.apply('index', ref); 19 | const parser = new TypescriptParser(); 20 | parser.parseSource(ts).then(ast => { 21 | expect(ast.exports.length).toBe(4); 22 | expect(ast.exports.some((ex: NamedExport) => ex.from === './api-configuration')).toBeDefined(); 23 | expect(ast.exports.some((ex: NamedExport) => ex.from === './request-builder')).toBeDefined(); 24 | expect(ast.exports.some((ex: NamedExport) => ex.from === './strict-http-response')).toBeDefined(); 25 | }); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/useTempDir.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import { OpenAPIObject } from '../lib/openapi-typings'; 3 | import options from './useTempDir.config.json'; 4 | import templatesSpec from './useTempDir.json'; 5 | import os from 'os'; 6 | 7 | const spec = templatesSpec as unknown as OpenAPIObject; 8 | 9 | describe('Generation tests using system temporary directory', () => { 10 | 11 | it('Use system temp folder when useTempDir is true', () => { 12 | 13 | const gen = new NgOpenApiGen(spec, options); 14 | gen.generate(); 15 | 16 | const tempDirectory = os.tmpdir(); 17 | 18 | expect(gen.tempDir.startsWith(tempDirectory)).toBe(true); 19 | expect(gen.tempDir.endsWith('useTempDir$')).toBe(true); 20 | 21 | }); 22 | 23 | it('Do not use system temp folder when useTempDir is false', () => { 24 | 25 | const optionsWithoutTempDir = { ...options }; 26 | optionsWithoutTempDir.useTempDir = false; 27 | 28 | const gen = new NgOpenApiGen(spec, optionsWithoutTempDir); 29 | gen.generate(); 30 | 31 | const tempDirectory = os.tmpdir(); 32 | expect(gen.tempDir.startsWith(tempDirectory)).toBe(false); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/self-ref.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Blah", 5 | "version": "1" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "Foo.Bar.Baz": { 11 | "type": "object", 12 | "required": [ 13 | "arrayProperty" 14 | ], 15 | "properties": { 16 | "refProperty": { 17 | "$ref": "#/components/schemas/Foo.Bar.Baz" 18 | }, 19 | "arrayProperty": { 20 | "type": "array", 21 | "items": { 22 | "$ref": "#/components/schemas/Foo.Bar.Baz" 23 | } 24 | }, 25 | "objectProperty": { 26 | "type": "object", 27 | "required": [ 28 | "nestedArray", 29 | "nestedRef" 30 | ], 31 | "properties": { 32 | "nestedArray": { 33 | "type": "array", 34 | "items": { 35 | "$ref": "#/components/schemas/Foo.Bar.Baz" 36 | } 37 | }, 38 | "nestedRef": { 39 | "$ref": "#/components/schemas/Foo.Bar.Baz" 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/duplicate-x-operation-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "ng-openapi-gen-duplicated-operation-name", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/api/car/consumption": { 9 | "get": { 10 | "x-controller-name": "Car", 11 | "x-operation-name": "getConsumption", 12 | "tags": ["Car"], 13 | "responses": { 14 | "200": { 15 | "description": "Consumption", 16 | "content": { 17 | "application/json": { 18 | "schema": { 19 | "type": "integer" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | }, 27 | "/api/plane/consumption": { 28 | "get": { 29 | "x-controller-name": "Plane", 30 | "x-operation-name": "getConsumption", 31 | "tags": ["Plane"], 32 | "responses": { 33 | "200": { 34 | "description": "Consumption", 35 | "content": { 36 | "application/json": { 37 | "schema": { 38 | "type": "integer" 39 | } 40 | } 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/functionIndex.spec.ts: -------------------------------------------------------------------------------- 1 | import { NamedExport, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import options from './functionIndex.config.json'; 5 | import templatesSpec from './functionIndex.json'; 6 | 7 | const spec = templatesSpec as unknown as OpenAPIObject; 8 | 9 | describe('Generation tests with index and no ApiModule', () => { 10 | const gen = new NgOpenApiGen(spec, options); 11 | 12 | beforeAll(() => { 13 | gen.generate(); 14 | }); 15 | 16 | it('functionIndex file', async () => { 17 | const operations = [...gen.operations.values()]; 18 | const functions = operations.reduce((opAcc, operation) => [ 19 | ...opAcc, 20 | ...operation.variants 21 | ], []); 22 | 23 | const ts = gen.templates.apply('functionIndex', {functions}); 24 | const parser = new TypescriptParser(); 25 | const ast = await parser.parseSource(ts); 26 | 27 | expect(ast.exports.length).toBe(2); // 1 type + 1 function 28 | expect(ast.exports.some((ex: NamedExport) => ex.from === './fn/operations/get-foos')).toBe(true); 29 | expect(ast.usages).toContain('GetFoos$Params'); 30 | expect(ast.usages).toContain('getFoos'); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /test/cmd-args.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseOptions } from '../lib/cmd-args'; 2 | 3 | describe('cmd-args.ts', () => { 4 | 5 | describe('#parseOptions', () => { 6 | 7 | it('should options contain customizedResponseType as an object if customizedResponseType is given in cmd args', () => { 8 | const sysArgs = ['--input', 'abc', '--customizedResponseType', '{}']; 9 | const options = parseOptions(sysArgs); 10 | expect(options.customizedResponseType).toEqual({}); 11 | }); 12 | 13 | it('should options contain customizedResponseType as an object if customizedResponseType is given in config file', () => { 14 | const sysArgs = ['--input', 'abc', '--config', 'test/cmd-args-test-config.json']; 15 | const options = parseOptions(sysArgs); 16 | expect(options.customizedResponseType).toEqual({ 17 | 'abc': { 18 | 'toUse': 'arraybuffer' 19 | } 20 | }); 21 | }); 22 | 23 | it('should customizedResponseType be overrided by cmd\'s if both config and args contains customizedResponseType', () => { 24 | const sysArgs = ['--input', 'abc', '--customizedResponseType', '{}', '--config', 'test/cmd-args-test-config.json']; 25 | const options = parseOptions(sysArgs); 26 | expect(options.customizedResponseType).toEqual({}); 27 | }); 28 | 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/allOf-required.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test using allOf to create an object type with required properties", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/foo": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "$ref": "#/components/schemas/Person" 16 | } 17 | }, 18 | "text/plain": {} 19 | } 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | "components": { 26 | "schemas": { 27 | "PartialPerson": { 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string" 32 | }, 33 | "nickname": { 34 | "type": "string" 35 | }, 36 | "age": { 37 | "type": "integer" 38 | } 39 | } 40 | }, 41 | "Person": { 42 | "allOf": [ 43 | { 44 | "$ref": "#/components/schemas/PartialPerson" 45 | }, 46 | { 47 | "type": "object", 48 | "required": [ 49 | "id", 50 | "nickname" 51 | ] 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/functionIndex.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Test with functionIndex", 5 | "version": "1.0" 6 | }, 7 | "paths": { 8 | "/foos": { 9 | "get": { 10 | "operationId": "getFoos", 11 | "parameters": [ 12 | { 13 | "name": "limit", 14 | "in": "query", 15 | "description": "How many items to return at one time (max 100)", 16 | "required": false, 17 | "schema": { 18 | "type": "integer", 19 | "format": "int32" 20 | } 21 | } 22 | ], 23 | "responses": { 24 | "200": { 25 | "description" : "Ok", 26 | "content": { 27 | "application/json": { 28 | "schema": { 29 | "$ref": "#/components/schemas/FooResponse" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "components": { 39 | "schemas" : { 40 | "FooResponse" : { 41 | "properties" : { 42 | "id" : { 43 | "type" : "integer", 44 | "format" : "int64" 45 | }, 46 | "name" : { 47 | "type" : "string" 48 | } 49 | } 50 | }, 51 | "Foos": { 52 | "type": "array", 53 | "items": { 54 | "$ref": "#/components/schemas/FooResponse" 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /templates/module.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { NgModule, ModuleWithProviders, SkipSelf, Optional } from '@angular/core'; 5 | import { HttpClient } from '@angular/common/http'; 6 | import { {{configurationClass}}, {{configurationParams}} } from './{{configurationFile}}'; 7 | 8 | {{#services}}import { {{typeName}} } from './services/{{fileName}}'; 9 | {{/services}} 10 | 11 | /** 12 | * Module that provides all services and configuration. 13 | */ 14 | @NgModule({ 15 | imports: [], 16 | exports: [], 17 | declarations: [], 18 | providers: [ 19 | {{#services}} {{typeName}}, 20 | {{/services}} 21 | {{configurationClass}} 22 | ], 23 | }) 24 | export class {{moduleClass}} { 25 | static forRoot(params: {{configurationParams}}): ModuleWithProviders<{{moduleClass}}> { 26 | return { 27 | ngModule: {{moduleClass}}, 28 | providers: [ 29 | { 30 | provide: {{configurationClass}}, 31 | useValue: params 32 | } 33 | ] 34 | } 35 | } 36 | 37 | constructor( 38 | @Optional() @SkipSelf() parentModule: {{moduleClass}}, 39 | @Optional() http: HttpClient 40 | ) { 41 | if (parentModule) { 42 | throw new Error('{{moduleClass}} is already loaded. Import in your base AppModule only.'); 43 | } 44 | if (!http) { 45 | throw new Error('You need to import the HttpClientModule in your AppModule! \n' + 46 | 'See also https://github.com/angular/angular/issues/20575'); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/parameter.ts: -------------------------------------------------------------------------------- 1 | import { escapeId, tsComments, tsType } from './gen-utils'; 2 | import { OpenAPIObject, ParameterObject } from './openapi-typings'; 3 | import { Options } from './options'; 4 | 5 | type ParameterLocation = 'query' | 'header' | 'path' | 'cookie'; 6 | 7 | /** 8 | * An operation parameter 9 | */ 10 | export class Parameter { 11 | 12 | var: string; 13 | varAccess: string; 14 | name: string; 15 | tsComments: string; 16 | required: boolean; 17 | in: ParameterLocation; 18 | type: string; 19 | style?: string; 20 | explode?: boolean; 21 | parameterOptions: string; 22 | specific = false; 23 | 24 | constructor(public spec: ParameterObject, options: Options, openApi: OpenAPIObject) { 25 | this.name = spec.name; 26 | this.var = escapeId(this.name); 27 | this.varAccess = this.var.includes('\'') ? `[${this.var}]` : `.${this.var}`; 28 | this.tsComments = tsComments(spec.description || '', 0, spec.deprecated); 29 | this.in = (spec.in || 'query') as ParameterLocation; 30 | this.required = this.in === 'path' || spec.required || false; 31 | this.type = tsType(spec.schema, options, openApi); 32 | this.style = spec.style; 33 | this.explode = spec.explode; 34 | this.parameterOptions = this.createParameterOptions(); 35 | } 36 | 37 | createParameterOptions(): string { 38 | const options: any = {}; 39 | if (this.style) { 40 | options.style = this.style; 41 | } 42 | if (!!this.explode === this.explode) { 43 | options.explode = this.explode; 44 | } 45 | return JSON.stringify(options); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/self-ref-allof.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Blah", 5 | "version": "1" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "Parent.Class": { 11 | "type": "object", 12 | "properties": { 13 | "parentProps": { 14 | "type": "string" 15 | } 16 | } 17 | }, 18 | "Foo.Bar.Baz": { 19 | "allOf": [{ "$ref": "#/components/schemas/Parent.Class" }, { 20 | "type": "object", 21 | "required": [ 22 | "arrayProperty" 23 | ], 24 | "properties": { 25 | "refProperty": { 26 | "$ref": "#/components/schemas/Foo.Bar.Baz" 27 | }, 28 | "arrayProperty": { 29 | "type": "array", 30 | "items": { 31 | "$ref": "#/components/schemas/Foo.Bar.Baz" 32 | } 33 | }, 34 | "objectProperty": { 35 | "type": "object", 36 | "required": [ 37 | "nestedArray", 38 | "nestedRef" 39 | ], 40 | "properties": { 41 | "nestedArray": { 42 | "type": "array", 43 | "items": { 44 | "$ref": "#/components/schemas/Foo.Bar.Baz" 45 | } 46 | }, 47 | "nestedRef": { 48 | "$ref": "#/components/schemas/Foo.Bar.Baz" 49 | } 50 | } 51 | } 52 | } 53 | }] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/default-success-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Test App", 5 | "description": "Test Description", 6 | "contact": { 7 | "name": "Marcell Kiss" 8 | }, 9 | "version": "1.0" 10 | }, 11 | "servers": [ 12 | { 13 | "url": "http://localhost:3000" 14 | } 15 | ], 16 | "paths": { 17 | "/path1": { 18 | "get": { 19 | "summary": "Get a default string response", 20 | "operationId": "getPath1", 21 | "responses": { 22 | "default": { 23 | "description": "default response", 24 | "content": { 25 | "application/json": { 26 | "schema": { 27 | "type": "string" 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }, 35 | "/path2": { 36 | "get": { 37 | "summary": "Get a default number response", 38 | "operationId": "getPath2", 39 | "responses": { 40 | "200": { 41 | "description": "default response", 42 | "content": { 43 | "application/json": { 44 | "schema": { 45 | "type": "number" 46 | } 47 | } 48 | } 49 | }, 50 | "default": { 51 | "description": "default response", 52 | "content": { 53 | "application/json": { 54 | "schema": { 55 | "type": "string" 56 | } 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "components": { 65 | "schemas": {} 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/duplicate-x-operation-name.spec.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import { Options } from '../lib/options'; 5 | import options from './duplicate-x-operation-name.config.json'; 6 | import spec from './duplicate-x-operation-name.json'; 7 | 8 | const gen = new NgOpenApiGen(spec as OpenAPIObject, options as Options); 9 | gen.generate(); 10 | 11 | describe('Generation tests using duplicate-x-operation-name.json', () => { 12 | it('index.ts should have both functions and parameters', () => { 13 | // Read file options.output + '/index.ts' 14 | const content = fs.readFileSync(options.output + '/index.ts', 'utf8'); 15 | expect(content).toContain('export { getConsumption as getConsumptionCar }'); 16 | expect(content).toContain('export type { GetConsumption$Params as GetConsumptionCar$Params }'); 17 | expect(content).toContain('export { getConsumption as getConsumptionPlane }'); 18 | expect(content).toContain('export type { GetConsumption$Params as GetConsumptionPlane$Params }'); 19 | }); 20 | 21 | it('functions.ts should have both functions and parameters', () => { 22 | // Read file options.output + '/functions.ts' 23 | const content = fs.readFileSync(options.output + '/functions.ts', 'utf8'); 24 | expect(content).toContain('export { getConsumption as getConsumptionCar }'); 25 | expect(content).toContain('export type { GetConsumption$Params as GetConsumptionCar$Params }'); 26 | expect(content).toContain('export { getConsumption as getConsumptionPlane }'); 27 | expect(content).toContain('export type { GetConsumption$Params as GetConsumptionPlane$Params }'); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/camelize-model-names.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { Options } from '../lib/options'; 4 | import options from './camelize-model-names.config.json'; 5 | import camelizeModelNamesSpec from './camelize-model-names.json'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = camelizeModelNamesSpec as unknown as OpenAPIObject; 9 | const gen = new NgOpenApiGen(spec, options as Options); 10 | gen.generate(); 11 | 12 | describe('Generation tests using camelize-model-names.json', () => { 13 | it('snake-case model', () => { 14 | const ref = gen.models.get('snake-case'); 15 | const ts = gen.templates.apply('model', ref); 16 | const parser = new TypescriptParser(); 17 | parser.parseSource(ts).then(ast => { 18 | expect(ast.imports.length).toBe(0); 19 | expect(ast.declarations.length).toBe(1); 20 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 21 | const decl = ast.declarations[0] as InterfaceDeclaration; 22 | expect(decl.name).toBe('PreSnake_casePos'); 23 | 24 | }); 25 | }); 26 | it('camelCase model', () => { 27 | const ref = gen.models.get('camelCase'); 28 | const ts = gen.templates.apply('model', ref); 29 | const parser = new TypescriptParser(); 30 | parser.parseSource(ts).then(ast => { 31 | expect(ast.imports.length).toBe(0); 32 | expect(ast.declarations.length).toBe(1); 33 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 34 | const decl = ast.declarations[0] as InterfaceDeclaration; 35 | expect(decl.name).toBe('PreCamelCasePos'); 36 | 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/skipJsonSuffix.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { Options } from '../lib/options'; 4 | import options from './skipJsonSuffix.config.json'; 5 | import skipJsonSuffixSpec from './skipJsonSuffix.json'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = skipJsonSuffixSpec as unknown as OpenAPIObject; 9 | const gen = new NgOpenApiGen(spec, options as Options); 10 | gen.generate(); 11 | 12 | 13 | describe('Generation tests using skipJsonSuffix.config', () => { 14 | 15 | it('Api', () => { 16 | const api = gen.services.get('Api'); 17 | expect(api).toBeDefined(); 18 | if (api) { 19 | const ts = gen.templates.apply('service', api); 20 | const parser = new TypescriptParser(); 21 | parser.parseSource(ts).then(ast => { 22 | expect(ast.declarations.length).toBe(1); 23 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 24 | const cls = ast.declarations[0] as ClassDeclaration; 25 | function assertMethodExists(name: string) { 26 | const method = cls.methods.find(m => m.name === name); 27 | expect(method).toBeDefined(); 28 | } function assertMethodNotExists(name: string) { 29 | const method = cls.methods.find(m => m.name === name); 30 | expect(method).toBeUndefined(); 31 | } 32 | assertMethodExists('fooGet$Response'); 33 | assertMethodExists('fooGet'); // Json 34 | assertMethodExists('fooGet$Plain'); 35 | assertMethodNotExists('fooGet$Json'); 36 | assertMethodExists('barGet$Response'); 37 | assertMethodExists('barGet'); 38 | assertMethodNotExists('barGet$Plain'); 39 | 40 | 41 | }); 42 | } 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /test/parameter.spec.ts: -------------------------------------------------------------------------------- 1 | import { Parameter } from '../lib/parameter'; 2 | 3 | const parameterNotExploded = new Parameter( 4 | { 5 | explode: false, 6 | name: 'par1', 7 | in: 'query', 8 | description: 'Description of par1', 9 | style: 'form', 10 | schema: { 11 | type: 'array', 12 | items: { 13 | type: 'string' 14 | } 15 | } 16 | }, 17 | { 18 | input: 'fake.json' 19 | }, 20 | { 21 | openapi: '', 22 | info: { 23 | title: 'fake open api', 24 | version: '3.0.0' 25 | }, 26 | paths: {} as any 27 | }); 28 | 29 | const parameterExploded = new Parameter( 30 | { 31 | explode: true, 32 | name: 'par1', 33 | in: 'query', 34 | description: 'Description of par1', 35 | style: 'form', 36 | schema: { 37 | type: 'array', 38 | items: { 39 | type: 'string' 40 | } 41 | } 42 | }, 43 | { 44 | input: 'fake.json' 45 | }, 46 | { 47 | openapi: '', 48 | info: { 49 | title: 'fake open api', 50 | version: '3.0.0' 51 | }, 52 | paths: {} as any 53 | }); 54 | 55 | const parameter = new Parameter( 56 | { 57 | name: 'par1', 58 | in: 'query', 59 | description: 'Description of par1', 60 | schema: { 61 | type: 'array', 62 | items: { 63 | type: 'string' 64 | } 65 | } 66 | }, 67 | { 68 | input: 'fake.json' 69 | }, 70 | { 71 | openapi: '', 72 | info: { 73 | title: 'fake open api', 74 | version: '3.0.0' 75 | }, 76 | paths: {} as any 77 | }); 78 | 79 | describe('Parameters constructor', () => { 80 | it('paramter options should be serialized', () => { 81 | expect(parameterNotExploded.parameterOptions).toBe('{"style":"form","explode":false}'); 82 | expect(parameterExploded.parameterOptions).toBe('{"style":"form","explode":true}'); 83 | expect(parameter.parameterOptions).toBe('{}'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /templates/fn.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; 5 | import { Observable } from 'rxjs'; 6 | import { filter, map } from 'rxjs/operators'; 7 | import { {{@root.responseClass}} } from '{{pathToRoot}}{{@root.responseFile}}'; 8 | import { {{@root.requestBuilderClass}} } from '{{pathToRoot}}{{@root.requestBuilderFile}}'; 9 | 10 | {{#imports}}import { {{{typeName}}}{{#useAlias}} as {{{qualifiedName}}}{{/useAlias}} } from '{{{@root.pathToRoot}}}{{{fullPath}}}'; 11 | {{/imports}} 12 | 13 | export interface {{paramsType}} { 14 | {{#operation.parameters}} 15 | {{{tsComments}}} {{{var}}}{{^required}}?{{/required}}: {{{type}}};{{#tsComments}}{{/tsComments}} 16 | {{/operation.parameters}} 17 | {{#requestBody}} 18 | {{{../operation.requestBody.tsComments}}}body{{^../operation.requestBody.required}}?{{/../operation.requestBody.required}}: {{{type}}} 19 | {{/requestBody}} 20 | } 21 | 22 | export function {{importName}}(http: HttpClient, rootUrl: string, params{{^operation.parametersRequired}}?{{/operation.parametersRequired}}: {{paramsType}}, context?: HttpContext): Observable<{{@root.responseClass}}<{{{resultType}}}>> { 23 | const rb = new {{@root.requestBuilderClass}}(rootUrl, {{importName}}.PATH, '{{operation.method}}'); 24 | if (params) { 25 | {{#operation.parameters}} 26 | rb.{{in}}('{{{name}}}', params{{{varAccess}}}, {{{parameterOptions}}}); 27 | {{/operation.parameters}} 28 | {{#requestBody}} 29 | rb.body(params.body, '{{{mediaType}}}'); 30 | {{/requestBody}} 31 | } 32 | 33 | return http.request( 34 | rb.build({ responseType: '{{{responseType}}}', accept: '{{{accept}}}', context }) 35 | ).pipe( 36 | filter((r: any): r is HttpResponse => r instanceof HttpResponse), 37 | map((r: HttpResponse) => { 38 | {{> handleResponse}} 39 | }) 40 | ); 41 | } 42 | 43 | {{importName}}.PATH = '{{{operation.path}}}'; 44 | -------------------------------------------------------------------------------- /test/templates.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './templates.config.json'; 3 | import templatesSpec from './templates.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = templatesSpec as unknown as OpenAPIObject; 9 | const gen = new NgOpenApiGen(spec, options as Options); 10 | const genCr = new NgOpenApiGen(spec, { ...options, endOfLineStyle: 'cr' } as Options); 11 | const genLf = new NgOpenApiGen(spec, { ...options, endOfLineStyle: 'lf' } as Options); 12 | const genCrlf = new NgOpenApiGen(spec, { ...options, endOfLineStyle: 'crlf' } as Options); 13 | 14 | gen.generate(); 15 | 16 | describe('Generation tests using templates.json', () => { 17 | 18 | it('Service template applied with custom Handlebars helper', () => { 19 | genCr.generate(); 20 | const fileContents = fs.readFileSync(fs.realpathSync(`${gen.outDir}/services/tag-1.service.ts`)); 21 | expect(/(Description of tag1)/ug.test(fileContents.toString())).toBe(true); 22 | }); 23 | 24 | it('Normalize end of line to cr', () => { 25 | genCr.generate(); 26 | const fileContents = fs.readFileSync(fs.realpathSync(`${genCr.outDir}/services/tag-1.service.ts`)); 27 | expect(/[\r]/.test(fileContents.toString())).toBe(true); 28 | expect(/[\n]/.test(fileContents.toString())).toBe(false); 29 | }); 30 | 31 | it('Normalize end of line to lf', () => { 32 | genLf.generate(); 33 | const fileContents = fs.readFileSync(fs.realpathSync(`${genLf.outDir}/services/tag-1.service.ts`)); 34 | expect(/[\r]/.test(fileContents.toString())).toBe(false); 35 | expect(/[\n]/.test(fileContents.toString())).toBe(true); 36 | }); 37 | 38 | it('Normalize end of line to crlf', () => { 39 | genCrlf.generate(); 40 | const fileContents = fs.readFileSync(fs.realpathSync(`${genCrlf.outDir}/services/tag-1.service.ts`)); 41 | expect(/[\r]/.test(fileContents.toString())).toBe(true); 42 | expect(/[\n]/.test(fileContents.toString())).toBe(true); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/default-success-response.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './default-success-response.config.json'; 3 | import defaultSuccessResponseSpec from './default-success-response.json'; 4 | import { OpenAPIObject } from '../lib/openapi-typings'; 5 | 6 | const spec = defaultSuccessResponseSpec as unknown as OpenAPIObject; 7 | const gen = new NgOpenApiGen(spec, options); 8 | gen.generate(); 9 | 10 | describe('Generation tests using default-success-response.json', () => { 11 | it('GET /path1 - default response can be a successResponse', () => { 12 | const operation = gen.operations.get('getPath1'); 13 | expect(operation).toBeDefined(); 14 | if (!operation) return; 15 | expect(operation.path).toBe('/path1'); 16 | expect(operation.method).toBe('get'); 17 | expect(operation.requestBody).toBeUndefined(); 18 | expect(operation.allResponses.length).toBe(1); 19 | const success = operation.successResponse; 20 | expect(success).toBeDefined(); 21 | if (!success) return; 22 | expect(success.statusCode).toBe('default'); 23 | const json = success.content.find(c => c.mediaType === 'application/json'); 24 | expect(json).toBeDefined(); 25 | if (json) { 26 | expect(json.type).toBe('string'); 27 | } 28 | }); 29 | 30 | it('GET /path2 - default response should not overwrite other successResponse', () => { 31 | const operation = gen.operations.get('getPath2'); 32 | expect(operation).toBeDefined(); 33 | if (!operation) return; 34 | expect(operation.path).toBe('/path2'); 35 | expect(operation.method).toBe('get'); 36 | expect(operation.requestBody).toBeUndefined(); 37 | expect(operation.allResponses.length).toBe(2); 38 | const success = operation.successResponse; 39 | expect(success).toBeDefined(); 40 | if (!success) return; 41 | expect(success.statusCode).toBe('200'); 42 | const json = success.content.find(c => c.mediaType === 'application/json'); 43 | expect(json).toBeDefined(); 44 | if (json) { 45 | expect(json.type).toBe('number'); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-openapi-gen", 3 | "version": "1.0.5", 4 | "license": "MIT", 5 | "author": "Cyclos development team", 6 | "description": "An OpenAPI 3.0 and 3.1 codegen for Angular 16+", 7 | "keywords": [ 8 | "angular", 9 | "openapi", 10 | "codegen" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/cyclosproject/ng-openapi-gen.git" 15 | }, 16 | "private": true, 17 | "bin": { 18 | "ng-openapi-gen": "lib/index.js" 19 | }, 20 | "main": "lib/ng-openapi-gen.js", 21 | "scripts": { 22 | "test": "vitest run", 23 | "test:watch": "vitest", 24 | "test:ui": "vitest --ui", 25 | "lint": "eslint 'lib/**' 'test/*.ts'", 26 | "compile": "tsc --project tsconfig.build.json && ncp \"LICENSE\" dist && ncp \"README.md\" \"dist/README.md\" && ncp \"templates\" \"dist/templates\" && ncp \"ng-openapi-gen-schema.json\" \"dist/ng-openapi-gen-schema.json\" && node scripts/prepare-dist-package.js && rimraf \"dist/test\"", 27 | "build": "npm run lint && npm run compile && npm test" 28 | }, 29 | "dependencies": { 30 | "@apidevtools/json-schema-ref-parser": "^14.2.1", 31 | "argparse": "^2.0.1", 32 | "eol": "^0.10.0", 33 | "fs-extra": "^11.3.2", 34 | "handlebars": "^4.7.8", 35 | "jsesc": "^3.1.0", 36 | "openapi-types": "^12.1.3", 37 | "lodash": "^4.17.21" 38 | }, 39 | "peerDependencies": { 40 | "@angular/core": ">=16.0.0", 41 | "rxjs": ">=6.5.0" 42 | }, 43 | "devDependencies": { 44 | "@types/argparse": "^2.0.17", 45 | "@types/fs-extra": "^11.0.4", 46 | "@types/jsesc": "^3.0.3", 47 | "@types/json-schema": "^7.0.15", 48 | "@types/lodash": "^4.17.20", 49 | "@types/node": "^24.9.0", 50 | "@typescript-eslint/eslint-plugin": "^8.46.2", 51 | "@typescript-eslint/parser": "^8.46.2", 52 | "eslint": "^9.38.0", 53 | "eslint-plugin-jsdoc": "^61.1.5", 54 | "vitest": "^3.2.4", 55 | "@vitest/ui": "^3.2.4", 56 | "ncp": "^2.0.0", 57 | "replace-in-file": "^8.3.0", 58 | "rimraf": "^6.0.1", 59 | "typescript-parser": "^2.6.1", 60 | "typescript": "~5.9.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import jsdoc from 'eslint-plugin-jsdoc'; 3 | import tsParser from '@typescript-eslint/parser'; 4 | 5 | export default [ 6 | { 7 | files: ['**/*.ts', '**/*.js'], 8 | ignores: ['node_modules/**', 'dist/**', 'out/**', '**/vitest.config.*'], 9 | languageOptions: { 10 | parser: tsParser, 11 | parserOptions: { 12 | project: './tsconfig.json', 13 | sourceType: 'module', 14 | ecmaVersion: 'latest', 15 | }, 16 | globals: { 17 | node: true, 18 | es6: true, 19 | }, 20 | }, 21 | plugins: { 22 | '@typescript-eslint': typescriptEslint, 23 | jsdoc: jsdoc, 24 | }, 25 | rules: { 26 | // Basic indentation and spacing 27 | indent: ['error', 2], 28 | 'brace-style': ['error', '1tbs'], 29 | 'eol-last': 'error', 30 | eqeqeq: ['error', 'smart'], 31 | 'guard-for-in': 'error', 32 | 'no-throw-literal': 'error', 33 | 'no-trailing-spaces': 'error', 34 | 'no-var': 'error', 35 | 'prefer-const': 'error', 36 | quotes: ['error', 'single'], 37 | semi: ['error', 'always'], 38 | 'spaced-comment': [ 39 | 'error', 40 | 'always', 41 | { 42 | markers: ['/'], 43 | }, 44 | ], 45 | 46 | // TypeScript-specific rules 47 | '@typescript-eslint/naming-convention': 'error', 48 | '@typescript-eslint/no-shadow': [ 49 | 'error', 50 | { 51 | hoist: 'all', 52 | }, 53 | ], 54 | '@typescript-eslint/prefer-namespace-keyword': 'error', 55 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 56 | '@typescript-eslint/no-explicit-any': 'off', // Allow any for this project 57 | '@typescript-eslint/explicit-function-return-type': 'off', 58 | '@typescript-eslint/explicit-module-boundary-types': 'off', 59 | 'no-shadow': 'off', // Turn off base rule to use TypeScript version 60 | 61 | // JSDoc rules 62 | 'jsdoc/check-alignment': 'error', 63 | 'jsdoc/check-indentation': 'error', 64 | }, 65 | }, 66 | ]; 67 | -------------------------------------------------------------------------------- /test/promises.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './promises.config.json'; 3 | import petstoreSpec from './petstore-3.0.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = petstoreSpec as unknown as OpenAPIObject; 9 | 10 | describe('Test promises generation', () => { 11 | 12 | it('promises', () => { 13 | const gen = new NgOpenApiGen(spec, { 14 | ...options, 15 | output: 'out/promises/' 16 | } as Options); 17 | gen.generate(); 18 | 19 | // Check that the generated service methods return Promises 20 | const serviceFileContent = fs.readFileSync(fs.realpathSync(`${gen.outDir}/services/pets.service.ts`), 'utf8'); 21 | 22 | // Check for Promise imports and return types 23 | expect(serviceFileContent).toContain('import { firstValueFrom } from \'rxjs\''); 24 | expect(serviceFileContent).not.toContain('import { Observable } from \'rxjs\''); 25 | 26 | // Check that response methods return Promise types 27 | expect(serviceFileContent).toMatch(/Promise/); 28 | expect(serviceFileContent).toContain('return firstValueFrom(obs)'); 29 | 30 | // Specifically check for response methods that should return Promises 31 | expect(serviceFileContent).toMatch(/listPets\$Response\(.*\): Promise/); 32 | expect(serviceFileContent).toMatch(/createPets\$Response\(.*\): Promise/); 33 | expect(serviceFileContent).toMatch(/showPetById\$Response\(.*\): Promise/); 34 | 35 | // The regular methods should also return Promise when promises are configured 36 | expect(serviceFileContent).toMatch(/listPets\(.*\): Promise<.*>/); 37 | expect(serviceFileContent).toMatch(/createPets\(.*\): Promise<.*>/); 38 | expect(serviceFileContent).toMatch(/showPetById\(.*\): Promise<.*>/); 39 | 40 | // Check that body methods use Promise.then() instead of .pipe() 41 | expect(serviceFileContent).toContain('.then((r: StrictHttpResponse<'); 42 | expect(serviceFileContent).not.toContain('.pipe('); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /lib/service.ts: -------------------------------------------------------------------------------- 1 | import { GenType } from './gen-type'; 2 | import { serviceClass, tsComments } from './gen-utils'; 3 | import { TagObject } from './openapi-typings'; 4 | import { Operation } from './operation'; 5 | import { Options } from './options'; 6 | 7 | /** 8 | * Context to generate a service 9 | */ 10 | export class Service extends GenType { 11 | 12 | constructor(tag: TagObject, public operations: Operation[], options: Options) { 13 | super(tag.name, serviceClass, options); 14 | 15 | // Angular standards demand that services have a period separating them 16 | if (this.fileName.endsWith('-service')) { 17 | this.fileName = this.fileName.substring(0, this.fileName.length - '-service'.length) + '.service'; 18 | } 19 | this.tsComments = tsComments(tag.description || '', 0); 20 | 21 | // Collect the imports 22 | for (const operation of operations) { 23 | operation.variants.forEach(variant => { 24 | // Import the variant fn 25 | this.addImport(variant); 26 | // Import the variant parameters 27 | this.addImport(variant.paramsImport); 28 | // Import the variant result type 29 | this.collectImports(variant.successResponse?.spec?.schema); 30 | // Add the request body additional dependencies 31 | this.collectImports(variant.requestBody?.spec?.schema, true); 32 | }); 33 | 34 | // Add the parameters as additional dependencies 35 | for (const parameter of operation.parameters) { 36 | this.collectImports(parameter.spec.schema, true); 37 | } 38 | 39 | // Add the responses imports as additional dependencies 40 | for (const resp of operation.allResponses) { 41 | for (const content of resp.content ?? []) { 42 | this.collectImports(content.spec?.schema, true); 43 | } 44 | } 45 | 46 | // Security schemes don't have schemas to import in newer OpenAPI versions 47 | // for (const securityGroup of operation.security) { 48 | // securityGroup.forEach(security => this.collectImports(security.spec.schema)); 49 | // } 50 | } 51 | this.updateImports(); 52 | } 53 | 54 | protected skipImport(): boolean { 55 | return false; 56 | } 57 | 58 | protected initPathToRoot(): string { 59 | return '../'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/observables.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './observables.config.json'; 3 | import petstoreSpec from './petstore-3.0.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = petstoreSpec as unknown as OpenAPIObject; 9 | 10 | describe('Test observables generation', () => { 11 | 12 | it('observables', () => { 13 | const gen = new NgOpenApiGen(spec, { 14 | ...options, 15 | output: 'out/observables/' 16 | } as Options); 17 | gen.generate(); 18 | 19 | // Check that the generated service methods return Observables 20 | const serviceFileContent = fs.readFileSync(fs.realpathSync(`${gen.outDir}/services/pets.service.ts`), 'utf8'); 21 | 22 | // Check for Observable imports and return types 23 | expect(serviceFileContent).toContain('import { Observable } from \'rxjs\''); 24 | expect(serviceFileContent).not.toContain('import { firstValueFrom } from \'rxjs\''); 25 | 26 | // Check that response methods return Observable types instead of Promise 27 | expect(serviceFileContent).toMatch(/Observable/); 28 | expect(serviceFileContent).toContain('return obs'); 29 | expect(serviceFileContent).not.toContain('return firstValueFrom(obs)'); 30 | 31 | // Specifically check for response methods that should return Observables 32 | expect(serviceFileContent).toMatch(/listPets\$Response\(.*\): Observable/); 33 | expect(serviceFileContent).toMatch(/createPets\$Response\(.*\): Observable/); 34 | expect(serviceFileContent).toMatch(/showPetById\$Response\(.*\): Observable/); 35 | 36 | // The regular methods should also return Observable 37 | expect(serviceFileContent).toMatch(/listPets\(.*\): Observable<.*>/); 38 | expect(serviceFileContent).toMatch(/createPets\(.*\): Observable<.*>/); 39 | expect(serviceFileContent).toMatch(/showPetById\(.*\): Observable<.*>/); 40 | 41 | // Check that body methods use .pipe() instead of Promise.then() 42 | expect(serviceFileContent).toContain('.pipe('); 43 | expect(serviceFileContent).not.toContain('.then((r: StrictHttpResponse<'); 44 | }); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/model-name-collision.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Model Name Collision Test", 5 | "version": "1" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "Clazz": { 11 | "type": "object", 12 | "properties": { 13 | "parentProps": { 14 | "type": "string" 15 | } 16 | } 17 | }, 18 | "Foo.Bar.Baz.Clazz": { 19 | "allOf": [ 20 | { "$ref": "#/components/schemas/Clazz" }, 21 | { 22 | "type": "object", 23 | "required": [ 24 | "arrayProperty" 25 | ], 26 | "properties": { 27 | "refProperty": { 28 | "$ref": "#/components/schemas/Foo.Bar.Baz.Clazz" 29 | }, 30 | "arrayProperty": { 31 | "type": "array", 32 | "items": { 33 | "$ref": "#/components/schemas/Foo.Bar.Baz.Clazz" 34 | } 35 | }, 36 | "parentRefProperty": { 37 | "$ref": "#/components/schemas/Clazz" 38 | }, 39 | "parentArrayProperty": { 40 | "type": "array", 41 | "items": { 42 | "$ref": "#/components/schemas/Clazz" 43 | } 44 | }, 45 | "objectProperty": { 46 | "type": "object", 47 | "required": [ 48 | "nestedArray", 49 | "nestedRef", 50 | "nestedParentRef" 51 | ], 52 | "properties": { 53 | "nestedArray": { 54 | "type": "array", 55 | "items": { 56 | "$ref": "#/components/schemas/Foo.Bar.Baz.Clazz" 57 | } 58 | }, 59 | "nestedRef": { 60 | "$ref": "#/components/schemas/Foo.Bar.Baz.Clazz" 61 | }, 62 | "nestedParentRef": { 63 | "$ref": "#/components/schemas/Clazz" 64 | }, 65 | "nestedParentArray": { 66 | "type": "array", 67 | "items": { 68 | "$ref": "#/components/schemas/Clazz" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | ] 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/model-name-collision.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import { OpenAPIObject } from '../lib/openapi-typings'; 3 | import options from './model-name-collision.config.json'; 4 | import modelNameCollisionSpec from './model-name-collision.json'; 5 | 6 | const spec = modelNameCollisionSpec as OpenAPIObject; 7 | 8 | describe('Model Name Collision Tests', () => { 9 | let gen: NgOpenApiGen; 10 | 11 | beforeAll(() => { 12 | gen = new NgOpenApiGen(spec, options); 13 | gen.generate(); 14 | }); 15 | 16 | it('should handle model name collision with imports', () => { 17 | // Check that both models exist 18 | const clazz = gen.models.get('Clazz'); 19 | const fooBarBazClazz = gen.models.get('Foo.Bar.Baz.Clazz'); 20 | 21 | expect(clazz).toBeDefined(); 22 | expect(fooBarBazClazz).toBeDefined(); 23 | 24 | if (fooBarBazClazz) { 25 | const ts = gen.templates.apply('model', fooBarBazClazz); 26 | 27 | // The generated code should use import aliases to avoid naming collisions 28 | // Should import base class with an alias: import { Clazz as Clazz_1 } from '../../../../models/clazz'; 29 | expect(ts).toContain('import { Clazz as Clazz_1 } from'); 30 | expect(ts).toContain('\'../../../../models/clazz\''); 31 | 32 | // Should export the current model with its correct name 33 | expect(ts).toContain('export type Clazz ='); 34 | 35 | // Should use the aliased import for the base type 36 | expect(ts).toContain('export type Clazz = Clazz_1 &'); 37 | 38 | // Should use the current model name for self-references 39 | expect(ts).toContain('\'refProperty\'?: Clazz;'); 40 | expect(ts).toContain('\'arrayProperty\': Array;'); 41 | 42 | // Should use the aliased import (Clazz_1) for parent class references 43 | expect(ts).toContain('\'parentRefProperty\'?: Clazz_1;'); 44 | expect(ts).toContain('\'parentArrayProperty\'?: Array;'); 45 | 46 | // Should use aliased import in nested object properties 47 | expect(ts).toContain('\'nestedParentRef\': Clazz_1;'); 48 | expect(ts).toContain('\'nestedParentArray\'?: Array;'); 49 | 50 | // Should still use current model name for self-references in nested objects 51 | expect(ts).toContain('\'nestedArray\': Array;'); 52 | expect(ts).toContain('\'nestedRef\': Clazz;'); 53 | 54 | // The generated code should be syntactically valid 55 | expect(ts).toBeTruthy(); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/allOf-required.spec.ts: -------------------------------------------------------------------------------- 1 | import { TypeAliasDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { Options } from '../lib/options'; 4 | import allOfRequired from './allOf-required.json'; 5 | import { OpenAPIObject } from '../lib/openapi-typings'; 6 | import options from './allOf-required.config.json'; 7 | 8 | const spec = allOfRequired as unknown as OpenAPIObject; 9 | const gen = new NgOpenApiGen(spec, options as Options); 10 | gen.generate(); 11 | 12 | describe('Generation tests using allOf-required.json', () => { 13 | it('Person model should have correct type alias structure', () => { 14 | const model = gen.models.get('Person'); 15 | expect(model).toBeDefined(); 16 | const ts = gen.templates.apply('model', model); 17 | const parser = new TypescriptParser(); 18 | parser.parseSource(ts).then(ast => { 19 | // Should have 3 declarations: Person$ internal type, RequiredProperties type, and Person export 20 | expect(ast.declarations.length).toBe(3); 21 | 22 | // Check Person$ internal type (should be the first declaration) 23 | const personInternalDecl = ast.declarations[0] as TypeAliasDeclaration; 24 | expect(personInternalDecl.name).toBe('Person$'); 25 | expect(personInternalDecl.isExported).toBe(false); 26 | 27 | // Check RequiredProperties declaration (should be the second declaration) 28 | const requiredPropsDecl = ast.declarations[1] as TypeAliasDeclaration; 29 | expect(requiredPropsDecl.name).toBe('RequiredProperties'); 30 | expect(requiredPropsDecl.isExported).toBe(false); 31 | 32 | // Check Person export (should be the last declaration) 33 | const personExport = ast.declarations[2]; 34 | expect(personExport).toEqual(expect.any(TypeAliasDeclaration)); 35 | const personDecl = personExport as TypeAliasDeclaration; 36 | expect(personDecl.name).toBe('Person'); 37 | expect(personDecl.isExported).toBe(true); 38 | }); 39 | }); 40 | 41 | it('Person model should contain intersection type with required properties', () => { 42 | const model = gen.models.get('Person'); 43 | expect(model).toBeDefined(); 44 | const ts = gen.templates.apply('model', model); 45 | // Check the generated TypeScript content structure 46 | expect(ts).toContain('type Person$ = PartialPerson & {'); 47 | expect(ts).toContain('type RequiredProperties = \'id\' | \'nickname\''); 48 | expect(ts).toContain('export type Person = Person$ & Required>'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/templates.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { Globals } from './globals'; 4 | 5 | /** 6 | * Holds all templates, and know how to apply them 7 | */ 8 | export class Templates { 9 | 10 | private templates: { [key: string]: HandlebarsTemplateDelegate } = {}; 11 | private globals: { [key: string]: any } = {}; 12 | 13 | constructor(builtInDir: string, customDir: string, handlebars: typeof Handlebars) { 14 | const builtInTemplates = fs.readdirSync(builtInDir); 15 | const customTemplates = customDir === '' ? [] : fs.readdirSync(customDir); 16 | // Read all built-in templates, but taking into account an override, if any 17 | for (const file of builtInTemplates) { 18 | const dir = customTemplates.includes(file) ? customDir : builtInDir; 19 | this.parseTemplate(dir, file, handlebars); 20 | } 21 | // Also read any custom templates which are not built-in 22 | for (const file of customTemplates) { 23 | this.parseTemplate(customDir, file, handlebars); 24 | } 25 | } 26 | 27 | private parseTemplate(dir: string, file: string, handlebars: typeof Handlebars) { 28 | const baseName = this.baseName(file); 29 | if (baseName) { 30 | const text = fs.readFileSync(path.join(dir, file), 'utf-8'); 31 | const compiled = handlebars.compile(text); 32 | this.templates[baseName] = compiled; 33 | handlebars.registerPartial(baseName, compiled); 34 | } 35 | } 36 | 37 | /** 38 | * Sets a global variable, that is, added to the model of all templates 39 | */ 40 | setGlobals(globals: Globals) { 41 | for (const name of Object.keys(globals)) { 42 | const value = (globals as { [key: string]: any })[name]; 43 | this.globals[name] = value; 44 | } 45 | } 46 | 47 | private baseName(file: string): string | null { 48 | if (!file.endsWith('.handlebars')) { 49 | return null; 50 | } 51 | return file.substring(0, file.length - '.handlebars'.length); 52 | } 53 | 54 | /** 55 | * Applies a template with a given model 56 | * @param templateName The template name (file without .handlebars extension) 57 | * @param model The model variables to be passed in to the template 58 | */ 59 | apply(templateName: string, model?: { [key: string]: any }): string { 60 | const template = this.templates[templateName]; 61 | if (!template) { 62 | throw new Error(`Template not found: ${templateName}`); 63 | } 64 | const actualModel: { [key: string]: any } = { ...this.globals, ...(model || {}) }; 65 | return template(actualModel); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /test/person-place.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0", 3 | "info": { 4 | "title": "Person and Place", 5 | "version": "1.0" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "Id": { 11 | "type": "string" 12 | }, 13 | "Entity": { 14 | "type": "object", 15 | "properties": { 16 | "id": { 17 | "$ref": "#/components/schemas/Id" 18 | } 19 | } 20 | }, 21 | "Person": { 22 | "allOf": [ 23 | { 24 | "$ref": "#/components/schemas/Entity" 25 | }, 26 | { 27 | "type": "object", 28 | "properties": { 29 | "name": { 30 | "type": "string" 31 | }, 32 | "places": { 33 | "type": "array", 34 | "items": { 35 | "$ref": "#/components/schemas/PersonPlace" 36 | } 37 | } 38 | } 39 | } 40 | ] 41 | }, 42 | "GPSLocation": { 43 | "type": "object", 44 | "properties": { 45 | "gps": { 46 | "type": "string", 47 | "description": "GPS coordinates" 48 | } 49 | } 50 | }, 51 | "Place": { 52 | "allOf": [ 53 | { 54 | "$ref": "#/components/schemas/Entity" 55 | }, 56 | { 57 | "oneOf": [ 58 | { 59 | "$ref": "#/components/schemas/GPSLocation" 60 | }, 61 | { 62 | "type": "object", 63 | "properties": { 64 | "address": { 65 | "type": "string", 66 | "description": "Street address" 67 | } 68 | } 69 | } 70 | ] 71 | }, 72 | { 73 | "type": "object", 74 | "properties": { 75 | "description": { 76 | "name": "string", 77 | "type": "string" 78 | } 79 | }, 80 | "additionalProperties": { 81 | "type": "string" 82 | } 83 | } 84 | ] 85 | }, 86 | "PersonPlace": { 87 | "type": "object", 88 | "properties": { 89 | "place": { 90 | "$ref": "#/components/schemas/Place" 91 | }, 92 | "since": { 93 | "description": "The date this place was assigned to the person", 94 | "type": "string", 95 | "format": "date" 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/mock/lib.ts: -------------------------------------------------------------------------------- 1 | export class Blob { 2 | type = 'application/json'; 3 | } 4 | 5 | export class File extends Blob { 6 | name = 'file.json'; 7 | } 8 | 9 | type FormDataEntryValue = File | string; 10 | 11 | export interface HttpParameterCodec { 12 | encodeKey(key: string): string; 13 | encodeValue(value: string): string; 14 | decodeKey(key: string): string; 15 | decodeValue(value: string): string; 16 | } 17 | 18 | export interface HttpParamsOptions { 19 | encoder?: HttpParameterCodec; 20 | } 21 | 22 | abstract class ParamContainer { 23 | private map = new Map(); 24 | 25 | constructor() { 26 | } 27 | 28 | has(param: string): boolean { 29 | return this.map.has(param); 30 | } 31 | 32 | get(param: string): T | null { 33 | const value = this.map.get(param); 34 | return value === undefined ? null : value[0]; 35 | } 36 | 37 | getAll(param: string): T[] | null { 38 | const value = this.map.get(param); 39 | return value === undefined ? null : value; 40 | } 41 | 42 | keys(): string[] { 43 | return Array.from(this.map.keys()); 44 | } 45 | 46 | append(param: string, value: T): this { 47 | if (!this.map.has(param)) { 48 | this.map.set(param, [value]); 49 | } else { 50 | this.map.set(param, [...(this.map.get(param) || []), value]); 51 | } 52 | return this; 53 | } 54 | set(param: string, value: T): this { 55 | this.map.set(param, [value]); 56 | return this; 57 | } 58 | 59 | delete(param: string, value?: T): this { 60 | if (value) { 61 | this.map.set(param, [...(this.map.get(param) || [])].filter(v => v !== value)); 62 | } else { 63 | this.map.delete(param); 64 | } 65 | return this; 66 | } 67 | 68 | toString(): string { 69 | return this.map.toString(); 70 | } 71 | } 72 | 73 | export class HttpParams extends ParamContainer { 74 | constructor(_options?: HttpParamsOptions) { 75 | super(); 76 | } 77 | } 78 | 79 | export class HttpHeaders extends ParamContainer { 80 | } 81 | 82 | export class HttpRequest<_T> { 83 | constructor(public method: string, public url: string, public body: string, public options: { 84 | params?: HttpParams, 85 | headers?: HttpHeaders, 86 | responseType?: 'json' | 'text' | 'blob' | 'arraybuffer', 87 | reportProgress?: boolean 88 | }) { 89 | } 90 | } 91 | 92 | export class FormData extends ParamContainer { 93 | forEach(callbackfn: (value: FormDataEntryValue, key: string, parent: FormData) => void, thisArg?: any): void { 94 | for (const key of this.keys()) { 95 | const value = this.get(key); 96 | if (value) { 97 | callbackfn.apply(thisArg || this, [value, key, this]); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/self-ref.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InterfaceDeclaration, 3 | TypeAliasDeclaration, 4 | TypescriptParser 5 | } from 'typescript-parser'; 6 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 7 | import options from './self-ref.config.json'; 8 | import selfRef from './self-ref.json'; 9 | 10 | import optionsAllof from './self-ref-allof.config.json'; 11 | import selfRefAllof from './self-ref-allof.json'; 12 | 13 | describe('Test self referencing', () => { 14 | describe('Generation tests using self-ref.json', () => { 15 | const gen = new NgOpenApiGen(selfRef as any, options); 16 | gen.generate(); 17 | it('Baz model', () => { 18 | const baz = gen.models.get('Foo.Bar.Baz'); 19 | const ts = gen.templates.apply('model', baz); 20 | 21 | const parser = new TypescriptParser(); 22 | parser.parseSource(ts).then(ast => { 23 | expect(ast.declarations.length).toBe(1); 24 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 25 | const decl = ast.declarations[0] as InterfaceDeclaration; 26 | expect(decl.name).toBe('Baz'); 27 | expect(decl.properties.length).toBe(3); 28 | 29 | const ref = decl.properties.find(p => p.name === 'refProperty'); 30 | expect(ref).toBeDefined(); 31 | if (ref) { 32 | expect(ref.type).toBe('Baz'); 33 | } 34 | 35 | const array = decl.properties.find(p => p.name === 'arrayProperty'); 36 | expect(array).toBeDefined(); 37 | if (array) { 38 | expect(array.type).toBe('Array'); 39 | } 40 | 41 | const object = decl.properties.find(p => p.name === 'objectProperty'); 42 | expect(object).toBeDefined(); 43 | if (object) { 44 | expect(object.type).toBe('{\n\'nestedArray\': Array;\n\'nestedRef\': Baz;\n}'); 45 | } 46 | 47 | 48 | }); 49 | }); 50 | 51 | }); 52 | 53 | describe('Generation tests using self-ref-allof.json', () => { 54 | const gen = new NgOpenApiGen(selfRefAllof as any, optionsAllof); 55 | gen.generate(); 56 | it('Baz model', () => { 57 | const baz = gen.models.get('Foo.Bar.Baz'); 58 | const ts = gen.templates.apply('model', baz); 59 | 60 | const parser = new TypescriptParser(); 61 | parser.parseSource(ts).then(ast => { 62 | expect(ast.declarations.length).toBe(1); 63 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 64 | const decl = ast.declarations[0] as TypeAliasDeclaration; 65 | expect(decl.name).toBe('Baz'); 66 | 67 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 68 | 69 | expect(text).toContain('\'refProperty\'?: Baz;'); 70 | expect(text).toContain('\'arrayProperty\': Array;'); 71 | expect(text).toContain('\'nestedArray\': Array;'); 72 | expect(text).toContain('\'nestedRef\': Baz;'); 73 | 74 | 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/polymorphic.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Blah", 5 | "version": "1" 6 | }, 7 | "paths": {}, 8 | "components": { 9 | "schemas": { 10 | "Foo.Bar.Tazk": { 11 | "type": "object", 12 | "allOf": [ 13 | { 14 | "$ref": "#/components/schemas/Foo.Bar.TazkBase" 15 | } 16 | ], 17 | "properties": { 18 | "taskNumber": { 19 | "type": "integer", 20 | "format": "int32" 21 | } 22 | }, 23 | "additionalProperties": false 24 | }, 25 | "Foo.Bar.TazkBase": { 26 | "type": "object", 27 | "properties": { 28 | "description": { 29 | "type": "string" 30 | } 31 | }, 32 | "additionalProperties": false 33 | }, 34 | "Foo.Bar.Dooz": { 35 | "type": "object", 36 | "properties": { 37 | "doozObject": { 38 | "type": "object", 39 | "allOf": [ 40 | { 41 | "$ref": "#/components/schemas/Foo.Bar.Tazk" 42 | } 43 | ], 44 | "properties": { 45 | "doozNumber": { 46 | "type": "integer", 47 | "format": "int32" 48 | } 49 | }, 50 | "additionalProperties": false 51 | } 52 | } 53 | }, 54 | "Foo.Bar.DiscBase": { 55 | "type": "object", 56 | "required": [ 57 | "$type" 58 | ], 59 | "properties": { 60 | "description": { 61 | "type": "string" 62 | }, 63 | "$type": { 64 | "type": "string" 65 | } 66 | }, 67 | "additionalProperties": false, 68 | "discriminator": { 69 | "propertyName": "$type", 70 | "mapping": { 71 | "disc-1": "#/components/schemas/Foo.Bar.DiscOne", 72 | "disc-2": "#/components/schemas/Foo.Bar.DiscTwo" 73 | } 74 | } 75 | }, 76 | "Foo.Bar.DiscOne": { 77 | "type": "object", 78 | "allOf": [ 79 | { 80 | "$ref": "#/components/schemas/Foo.Bar.DiscBase" 81 | } 82 | ], 83 | "properties": { 84 | "discNumber": { 85 | "type": "integer", 86 | "format": "int32" 87 | } 88 | }, 89 | "additionalProperties": false 90 | }, 91 | "Foo.Bar.DiscTwo": { 92 | "type": "object", 93 | "allOf": [ 94 | { 95 | "$ref": "#/components/schemas/Foo.Bar.DiscBase" 96 | } 97 | ], 98 | "properties": { 99 | "discText": { 100 | "type": "string" 101 | } 102 | }, 103 | "additionalProperties": false 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/globals.ts: -------------------------------------------------------------------------------- 1 | import { fileName } from './gen-utils'; 2 | import { Options } from './options'; 3 | 4 | /** 5 | * Stores the global variables used on generation 6 | */ 7 | export class Globals { 8 | 9 | configurationClass: string; 10 | configurationFile: string; 11 | configurationParams: string; 12 | baseServiceClass: string; 13 | baseServiceFile: string; 14 | apiServiceClass?: string; 15 | apiServiceFile?: string; 16 | requestBuilderClass: string; 17 | requestBuilderFile: string; 18 | responseClass: string; 19 | responseFile: string; 20 | moduleClass?: string; 21 | moduleFile?: string; 22 | modelIndexFile?: string; 23 | functionIndexFile?: string; 24 | serviceIndexFile?: string; 25 | rootUrl?: string; 26 | promises: boolean; 27 | generateServices: boolean; 28 | 29 | constructor(options: Options) { 30 | this.configurationClass = options.configuration ?? 'ApiConfiguration'; 31 | this.configurationFile = fileName(this.configurationClass); 32 | this.configurationParams = `${this.configurationClass}Params`; 33 | this.baseServiceClass = options.baseService ?? 'BaseService'; 34 | this.baseServiceFile = fileName(this.baseServiceClass); 35 | if (options.apiService === false) { 36 | this.apiServiceClass = undefined; 37 | } else { 38 | this.apiServiceClass = options.apiService === true ? '' : options.apiService; 39 | if ((this.apiServiceClass ?? '') === '') { 40 | this.apiServiceClass = 'Api'; 41 | } 42 | if (typeof this.apiServiceClass === 'string') { 43 | // Angular's best practices demands xxx.service.ts, not xxx-service.ts 44 | this.apiServiceFile = fileName(this.apiServiceClass).replace(/\-service$/, '.service'); 45 | } 46 | } 47 | this.promises = options.promises ?? true; 48 | this.requestBuilderClass = options.requestBuilder ?? 'RequestBuilder'; 49 | this.requestBuilderFile = fileName(this.requestBuilderClass); 50 | this.responseClass = options.response ?? 'StrictHttpResponse'; 51 | this.responseFile = fileName(this.responseClass); 52 | if (options.module !== false && options.module !== '') { 53 | this.moduleClass = options.module === true || options.module === undefined ? 'ApiModule' : options.module; 54 | // Angular's best practices demands xxx.module.ts, not xxx-module.ts 55 | this.moduleFile = fileName(this.moduleClass as string).replace(/\-module$/, '.module'); 56 | } 57 | if (options.serviceIndex !== false && options.serviceIndex !== '') { 58 | this.serviceIndexFile = options.serviceIndex === true || options.serviceIndex === undefined ? 'services' : options.serviceIndex; 59 | } 60 | if (options.modelIndex !== false && options.modelIndex !== '') { 61 | this.modelIndexFile = options.modelIndex === true || options.modelIndex === undefined ? 'models' : options.modelIndex; 62 | } 63 | if (options.functionIndex !== false && options.functionIndex !== '') { 64 | this.functionIndexFile = options.functionIndex === true || options.functionIndex === undefined ? 'functions' : options.functionIndex; 65 | } 66 | this.generateServices = !!options.services; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/discriminator-inheritance.spec.ts: -------------------------------------------------------------------------------- 1 | import { TypeAliasDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import { Options } from '../lib/options'; 5 | import options from './discriminator-inheritance.config.json'; 6 | import spec from './discriminator-inheritance.json'; 7 | 8 | const gen = new NgOpenApiGen(spec as OpenAPIObject, options as Options); 9 | gen.generate(); 10 | 11 | describe('Generation tests using discriminator-inheritance.json', () => { 12 | it('Product3 model should have discriminator property', () => { 13 | const model = gen.models.get('Product3'); 14 | expect(model).toBeDefined(); 15 | const ts = gen.templates.apply('model', model); 16 | const parser = new TypescriptParser(); 17 | parser.parseSource(ts).then(ast => { 18 | expect(ast.declarations.length).toBe(1); 19 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 20 | const decl = ast.declarations[0] as TypeAliasDeclaration; 21 | expect(decl.name).toBe('Product3'); 22 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 23 | 24 | // Product3 should have the discriminator property 'code': 'PRODUCT3' 25 | expect(text).toContain('\'code\': \'PRODUCT3\''); 26 | expect(text).toContain('\'number\'?: string'); 27 | }); 28 | }); 29 | 30 | it('Product4 model should have discriminator property (second level inheritance)', () => { 31 | const model = gen.models.get('Product4'); 32 | expect(model).toBeDefined(); 33 | const ts = gen.templates.apply('model', model); 34 | const parser = new TypescriptParser(); 35 | parser.parseSource(ts).then(ast => { 36 | expect(ast.declarations.length).toBe(1); 37 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 38 | const decl = ast.declarations[0] as TypeAliasDeclaration; 39 | expect(decl.name).toBe('Product4'); 40 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 41 | 42 | // Product4 should have the discriminator property 'code': 'PRODUCT4' 43 | expect(text).toContain('\'code\': \'PRODUCT4\''); 44 | expect(text).toContain('\'value\'?: OtherObject'); 45 | }); 46 | }); 47 | 48 | it('Product1 model should have discriminator property (first level inheritance)', () => { 49 | const model = gen.models.get('Product1'); 50 | expect(model).toBeDefined(); 51 | const ts = gen.templates.apply('model', model); 52 | const parser = new TypescriptParser(); 53 | parser.parseSource(ts).then(ast => { 54 | expect(ast.declarations.length).toBe(1); 55 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 56 | const decl = ast.declarations[0] as TypeAliasDeclaration; 57 | expect(decl.name).toBe('Product1'); 58 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 59 | 60 | // Product1 should have the discriminator property 'code': 'PRODUCT1' 61 | expect(text).toContain('\'code\': \'PRODUCT1\''); 62 | expect(text).toContain('\'name\'?: string'); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /templates/apiService.handlebars: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* Code generated by ng-openapi-gen DO NOT EDIT. */ 3 | 4 | import { Injectable } from '@angular/core'; 5 | import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; 6 | import { Observable{{#if promises}}, firstValueFrom{{/if}} } from 'rxjs'; 7 | import { filter, map } from 'rxjs/operators'; 8 | import { {{configurationClass}} } from './{{configurationFile}}'; 9 | import { {{responseClass}} } from './{{responseFile}}'; 10 | 11 | export type ApiFnOptional = (http: HttpClient, rootUrl: string, params?: P, context?: HttpContext) => Observable>; 12 | export type ApiFnRequired = (http: HttpClient, rootUrl: string, params: P, context?: HttpContext) => Observable>; 13 | 14 | /** 15 | * Helper service to call API functions directly 16 | */ 17 | @Injectable({ providedIn: 'root' }) 18 | export class {{apiServiceClass}} { 19 | constructor( 20 | private config: {{configurationClass}}, 21 | private http: HttpClient 22 | ) { 23 | } 24 | 25 | private _rootUrl?: string; 26 | 27 | /** 28 | * Returns the root url for API operations. If not set directly here, 29 | * will fallback to `{{configurationClass}}.rootUrl`. 30 | */ 31 | get rootUrl(): string { 32 | return this._rootUrl || this.config.rootUrl; 33 | } 34 | 35 | /** 36 | * Sets the root URL for API operations 37 | */ 38 | set rootUrl(rootUrl: string) { 39 | this._rootUrl = rootUrl; 40 | } 41 | 42 | /** 43 | * Executes an API call, returning the response body only 44 | */ 45 | invoke(fn: ApiFnRequired, params: P, context?: HttpContext): {{#if promises}}Promise{{else}}Observable{{/if}}; 46 | invoke(fn: ApiFnOptional, params?: P, context?: HttpContext): {{#if promises}}Promise{{else}}Observable{{/if}}; 47 | {{#if promises}}async {{/if}}invoke(fn: ApiFnRequired | ApiFnOptional, params: P, context?: HttpContext): {{#if promises}}Promise{{else}}Observable{{/if}} { 48 | const resp = this.invoke$Response(fn, params, context); 49 | {{#if promises}} 50 | return (await resp).body; 51 | {{else}} 52 | return resp.pipe(map(r => r.body)); 53 | {{/if}} 54 | } 55 | 56 | /** 57 | * Executes an API call, returning the entire response 58 | */ 59 | invoke$Response(fn: ApiFnRequired, params: P, context?: HttpContext): {{#if promises}}Promise>{{else}}Observable>{{/if}}; 60 | invoke$Response(fn: ApiFnOptional, params?: P, context?: HttpContext): {{#if promises}}Promise>{{else}}Observable>{{/if}}; 61 | invoke$Response(fn: ApiFnRequired | ApiFnOptional, params: P, context?: HttpContext): {{#if promises}}Promise>{{else}}Observable>{{/if}} { 62 | const obs = fn(this.http, this.rootUrl, params, context) 63 | .pipe( 64 | filter(r => r instanceof HttpResponse), 65 | map(r => r as StrictHttpResponse)); 66 | {{#if promises}} 67 | return firstValueFrom(obs); 68 | {{else}} 69 | return obs; 70 | {{/if}} 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/imports.ts: -------------------------------------------------------------------------------- 1 | import { modelFile, qualifiedName, unqualifiedName } from './gen-utils'; 2 | import { Importable } from './importable'; 3 | import { Options } from './options'; 4 | 5 | /** A general import */ 6 | export class Import implements Importable { 7 | name: string; 8 | typeName: string; 9 | qualifiedName: string; 10 | path: string; 11 | file: string; 12 | useAlias: boolean; 13 | fullPath: string; 14 | typeOnly: boolean; 15 | 16 | // Fields from Importable 17 | importName: string; 18 | importPath: string; 19 | importFile: string; 20 | importTypeName?: string; 21 | importQualifiedName?: string; 22 | 23 | constructor(name: string, typeName: string, qName: string, path: string, file: string, typeOnly: boolean) { 24 | this.name = name; 25 | this.typeName = typeName; 26 | this.qualifiedName = qName; 27 | this.useAlias = this.typeName !== this.qualifiedName; 28 | this.typeOnly = typeOnly; 29 | this.path = path; 30 | this.file = file; 31 | this.fullPath = `${this.path.split('/').filter(p => p.length).join('/')}/${this.file.split('/').filter(p => p.length).join('/')}`; 32 | 33 | this.importName = name; 34 | this.importPath = path; 35 | this.importFile = file; 36 | this.importTypeName = typeName; 37 | this.importQualifiedName = qName; 38 | } 39 | } 40 | 41 | /** 42 | * Manages the model imports to be added to a generated file 43 | */ 44 | export class Imports { 45 | private _imports = new Map(); 46 | 47 | constructor(private options: Options, private currentTypeName?: string) { 48 | } 49 | 50 | /** 51 | * Adds an import 52 | */ 53 | add(param: string | Importable, typeOnly: boolean) { 54 | let imp: Import; 55 | if (typeof param === 'string') { 56 | // A model 57 | const importTypeName = unqualifiedName(param, this.options); 58 | let importQualifiedName = qualifiedName(param, this.options); 59 | 60 | // Check for collision with current type name 61 | if (this.currentTypeName && importTypeName === this.currentTypeName) { 62 | // Add suffix to avoid collision 63 | let suffix = 1; 64 | let aliasedTypeName = `${importTypeName}_${suffix}`; 65 | while (this.hasImportWithTypeName(aliasedTypeName)) { 66 | suffix++; 67 | aliasedTypeName = `${importTypeName}_${suffix}`; 68 | } 69 | // Keep the original typeName for import, use alias for qualifiedName 70 | importQualifiedName = aliasedTypeName; 71 | } 72 | 73 | imp = new Import(param, importTypeName, importQualifiedName, 'models/', modelFile(param, this.options), typeOnly); 74 | } else { 75 | // An Importable 76 | imp = new Import(param.importName, param.importTypeName ?? param.importName, param.importQualifiedName ?? param.importName, `${param.importPath}`, param.importFile, typeOnly); 77 | } 78 | this._imports.set(imp.name, imp); 79 | } 80 | 81 | private hasImportWithTypeName(typeName: string): boolean { 82 | for (const imp of this._imports.values()) { 83 | if (imp.qualifiedName === typeName) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | } 89 | 90 | toArray(): Import[] { 91 | const array = [...this._imports.values()]; 92 | array.sort((a, b) => a.importName.localeCompare(b.importName, 'en')); 93 | return array; 94 | } 95 | 96 | get size() { 97 | return this._imports.size; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/discriminator-inheritance.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Discriminator Inheritance Test", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/products": { 9 | "get": { 10 | "responses": { 11 | "200": { 12 | "description": "Success", 13 | "content": { 14 | "application/json": { 15 | "schema": { 16 | "oneOf": [ 17 | { 18 | "$ref": "#/components/schemas/Product1" 19 | }, 20 | { 21 | "$ref": "#/components/schemas/Product2" 22 | }, 23 | { 24 | "$ref": "#/components/schemas/Product3" 25 | }, 26 | { 27 | "$ref": "#/components/schemas/Product4" 28 | } 29 | ] 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }, 38 | "components": { 39 | "schemas": { 40 | "Product": { 41 | "type": "object", 42 | "required": ["code"], 43 | "properties": { 44 | "code": { 45 | "type": "string", 46 | "example": "PRODUCT1" 47 | } 48 | }, 49 | "discriminator": { 50 | "propertyName": "code", 51 | "mapping": { 52 | "PRODUCT1": "#/components/schemas/Product1", 53 | "PRODUCT2": "#/components/schemas/Product2", 54 | "PRODUCT3": "#/components/schemas/Product3", 55 | "PRODUCT4": "#/components/schemas/Product4" 56 | } 57 | } 58 | }, 59 | "Product1": { 60 | "type": "object", 61 | "allOf": [ 62 | { 63 | "$ref": "#/components/schemas/Product" 64 | }, 65 | { 66 | "properties": { 67 | "name": { 68 | "type": "string", 69 | "example": "Product 1" 70 | } 71 | } 72 | } 73 | ] 74 | }, 75 | "Product2": { 76 | "type": "object", 77 | "allOf": [ 78 | { 79 | "$ref": "#/components/schemas/Product" 80 | }, 81 | { 82 | "properties": { 83 | "description": { 84 | "type": "string", 85 | "example": "Product 2 description" 86 | } 87 | } 88 | } 89 | ] 90 | }, 91 | "Product3": { 92 | "type": "object", 93 | "allOf": [ 94 | { 95 | "$ref": "#/components/schemas/Product" 96 | }, 97 | { 98 | "properties": { 99 | "number": { 100 | "type": "string", 101 | "example": "453091G265555000" 102 | } 103 | } 104 | } 105 | ] 106 | }, 107 | "Product4": { 108 | "type": "object", 109 | "allOf": [ 110 | { 111 | "$ref": "#/components/schemas/Product3" 112 | }, 113 | { 114 | "properties": { 115 | "value": { 116 | "$ref": "#/components/schemas/OtherObject" 117 | } 118 | } 119 | } 120 | ] 121 | }, 122 | "OtherObject": { 123 | "type": "object", 124 | "properties": { 125 | "someProperty": { 126 | "type": "string" 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /lib/gen-type.ts: -------------------------------------------------------------------------------- 1 | import { fileName, namespace, simpleName, typeName } from './gen-utils'; 2 | import { Importable } from './importable'; 3 | import { Import, Imports } from './imports'; 4 | import { ReferenceObject, SchemaObject, isReferenceObject, isArraySchemaObject } from './openapi-typings'; 5 | import { Options } from './options'; 6 | 7 | /** 8 | * Base definitions of a generated type 9 | */ 10 | export abstract class GenType { 11 | 12 | /** Name of the generated type / class */ 13 | typeName: string; 14 | 15 | /** Namespace, separated by '/' */ 16 | namespace?: string; 17 | 18 | /** Camel-case qualified name of the type, including namespace */ 19 | qualifiedName: string; 20 | 21 | /** Name of the generated file */ 22 | fileName: string; 23 | 24 | /** TypeScript comments for this type */ 25 | tsComments: string; 26 | 27 | pathToRoot: string; 28 | 29 | imports: Import[]; 30 | private _imports: Imports; 31 | 32 | additionalDependencies: string[]; 33 | private _additionalDependencies = new Set(); 34 | 35 | constructor( 36 | public name: string, 37 | typeNameTransform: (typeName: string, options: Options) => string, 38 | public options: Options) { 39 | 40 | this.typeName = typeNameTransform(name, options); 41 | this.namespace = namespace(name); 42 | this.fileName = fileName(this.typeName); 43 | this.qualifiedName = this.typeName; 44 | if (this.namespace) { 45 | this.fileName = this.namespace + '/' + this.fileName; 46 | this.qualifiedName = typeName(this.namespace, options) + this.typeName; 47 | } 48 | this._imports = new Imports(options, this.typeName); 49 | } 50 | 51 | protected addImport(param: string | Importable | null | undefined, typeOnly?: boolean) { 52 | if (param && !this.skipImport(param)) { 53 | this._imports.add(param, !!typeOnly); 54 | } 55 | } 56 | 57 | protected abstract skipImport(name: string | Importable): boolean; 58 | 59 | protected abstract initPathToRoot(): string; 60 | 61 | protected updateImports() { 62 | this.pathToRoot = this.initPathToRoot(); 63 | this.imports = this._imports.toArray(); 64 | for (const imp of this.imports) { 65 | imp.path = this.pathToRoot + imp.path; 66 | } 67 | this.additionalDependencies = [...this._additionalDependencies]; 68 | } 69 | 70 | protected collectImports(schema: SchemaObject | ReferenceObject | undefined, additional = false, processOneOf = false): void { 71 | if (!schema) { 72 | return; 73 | } else if (isReferenceObject(schema)) { 74 | const dep = simpleName(schema.$ref); 75 | if (additional) { 76 | this._additionalDependencies.add(dep); 77 | } else { 78 | this.addImport(dep); 79 | } 80 | } else { 81 | schema = schema as SchemaObject; 82 | (schema.oneOf || []).forEach(i => this.collectImports(i, additional)); 83 | (schema.allOf || []).forEach(i => this.collectImports(i, additional)); 84 | (schema.anyOf || []).forEach(i => this.collectImports(i, additional)); 85 | if (processOneOf) { 86 | (schema.oneOf || []).forEach(i => this.collectImports(i, additional)); 87 | } 88 | if (isArraySchemaObject(schema) && 'items' in schema) { 89 | this.collectImports(schema.items, additional); 90 | } 91 | if (schema.properties) { 92 | const properties = schema.properties; 93 | Object.keys(properties).forEach(p => { 94 | const prop = properties[p]; 95 | this.collectImports(prop, additional, true); 96 | }); 97 | } 98 | if (typeof schema.additionalProperties === 'object') { 99 | this.collectImports(schema.additionalProperties, additional); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/cbor-duplicate-methods.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "CBOR Duplicate Methods Test", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/search": { 9 | "post": { 10 | "operationId": "searchPost", 11 | "summary": "Search with CBOR request and response", 12 | "description": "This operation uses application/cbor for both request and response, which was causing duplicate methods in generated services", 13 | "requestBody": { 14 | "required": true, 15 | "content": { 16 | "application/cbor": { 17 | "schema": { 18 | "$ref": "#/components/schemas/SearchRequest" 19 | } 20 | } 21 | } 22 | }, 23 | "responses": { 24 | "200": { 25 | "description": "Successful response", 26 | "content": { 27 | "application/cbor": { 28 | "schema": { 29 | "$ref": "#/components/schemas/ResponseModel" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | }, 37 | "/mixed-content": { 38 | "post": { 39 | "operationId": "mixedContent", 40 | "summary": "Mixed content types to ensure proper variant generation", 41 | "requestBody": { 42 | "required": true, 43 | "content": { 44 | "application/json": { 45 | "schema": { 46 | "$ref": "#/components/schemas/SearchRequest" 47 | } 48 | }, 49 | "application/cbor": { 50 | "schema": { 51 | "$ref": "#/components/schemas/SearchRequest" 52 | } 53 | } 54 | } 55 | }, 56 | "responses": { 57 | "200": { 58 | "description": "Successful response", 59 | "content": { 60 | "application/json": { 61 | "schema": { 62 | "$ref": "#/components/schemas/ResponseModel" 63 | } 64 | }, 65 | "application/cbor": { 66 | "schema": { 67 | "$ref": "#/components/schemas/ResponseModel" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "components": { 77 | "schemas": { 78 | "SearchRequest": { 79 | "type": "object", 80 | "properties": { 81 | "query": { 82 | "type": "string", 83 | "description": "Search query string" 84 | }, 85 | "filters": { 86 | "type": "object", 87 | "additionalProperties": { 88 | "type": "string" 89 | }, 90 | "description": "Optional search filters" 91 | } 92 | }, 93 | "required": ["query"] 94 | }, 95 | "ResponseModel": { 96 | "type": "object", 97 | "properties": { 98 | "results": { 99 | "type": "array", 100 | "items": { 101 | "type": "object", 102 | "properties": { 103 | "id": { 104 | "type": "string" 105 | }, 106 | "title": { 107 | "type": "string" 108 | }, 109 | "score": { 110 | "type": "number" 111 | } 112 | } 113 | } 114 | }, 115 | "totalCount": { 116 | "type": "integer", 117 | "description": "Total number of results" 118 | } 119 | }, 120 | "required": ["results", "totalCount"] 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/openapi-typings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Centralized OpenAPI type definitions and utilities for both OpenAPI 3.0 and 3.1 support 3 | */ 4 | import { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'; 5 | 6 | // === Core OpenAPI Type Definitions === 7 | export type OpenAPIObject = OpenAPIV3.Document | OpenAPIV3_1.Document; 8 | export type OperationObject = OpenAPIV3.OperationObject | OpenAPIV3_1.OperationObject; 9 | export type PathsObject = OpenAPIV3.PathsObject | OpenAPIV3_1.PathsObject; 10 | export type PathItemObject = OpenAPIV3.PathItemObject | OpenAPIV3_1.PathItemObject; 11 | export type ReferenceObject = OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject; 12 | export type SchemaObject = OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject; 13 | export type ParameterObject = OpenAPIV3.ParameterObject | OpenAPIV3_1.ParameterObject; 14 | export type RequestBodyObject = OpenAPIV3.RequestBodyObject | OpenAPIV3_1.RequestBodyObject; 15 | export type ResponseObject = OpenAPIV3.ResponseObject | OpenAPIV3_1.ResponseObject; 16 | export type MediaTypeObject = OpenAPIV3.MediaTypeObject | OpenAPIV3_1.MediaTypeObject; 17 | export type SecuritySchemeObject = OpenAPIV3.SecuritySchemeObject | OpenAPIV3_1.SecuritySchemeObject; 18 | export type SecurityRequirementObject = OpenAPIV3.SecurityRequirementObject | OpenAPIV3_1.SecurityRequirementObject; 19 | export type TagObject = OpenAPIV3.TagObject | OpenAPIV3_1.TagObject; 20 | export type ContentObject = {[media: string]: MediaTypeObject}; 21 | export type ArraySchemaObject = OpenAPIV3.ArraySchemaObject | OpenAPIV3_1.ArraySchemaObject; 22 | export type ApiKeySecurityScheme = OpenAPIV3.ApiKeySecurityScheme | OpenAPIV3_1.ApiKeySecurityScheme; 23 | 24 | // === Type Guard Functions === 25 | // These functions provide safe type checking for OpenAPI objects 26 | 27 | /** 28 | * Type guard to check if an object is a ReferenceObject 29 | */ 30 | export function isReferenceObject(obj: any): obj is ReferenceObject { 31 | return obj && typeof obj === 'object' && '$ref' in obj; 32 | } 33 | 34 | /** 35 | * Type guard to check if a schema is an ArraySchemaObject 36 | */ 37 | export function isArraySchemaObject(obj: SchemaObject): obj is ArraySchemaObject { 38 | if (!('type' in obj)) { 39 | return false; 40 | } 41 | if (!('items' in obj)) { 42 | return false; 43 | } 44 | if (obj.type === 'array') { 45 | return true; 46 | } 47 | // OpenAPI 3.1 allows 'type' to be an array of types, so we need to check if it includes 'array' 48 | if (Array.isArray(obj.type) && obj.type.includes('array')) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | /** 55 | * Checks if a schema is nullable (compatible with both OpenAPI 3.0 and 3.1) 56 | * OpenAPI 3.0 uses 'nullable: true', OpenAPI 3.1 uses 'type: [T, "null"]' 57 | */ 58 | export function isNullable(schema: SchemaObject): boolean { 59 | // OpenAPI 3.0 style: nullable property 60 | if ('nullable' in schema && schema.nullable === true) { 61 | return true; 62 | } 63 | 64 | // OpenAPI 3.1 style: type array with "null" 65 | if ('type' in schema && Array.isArray(schema.type)) { 66 | return schema.type.includes('null' as any); 67 | } 68 | 69 | return false; 70 | } 71 | 72 | /** 73 | * Safely extracts the type from a schema object 74 | */ 75 | export function getSchemaType(schema: SchemaObject): string | string[] { 76 | if ('type' in schema && schema.type) { 77 | if (Array.isArray(schema.type)) { 78 | // OpenAPI 3.1 style - return all types for union handling 79 | return schema.type; 80 | } 81 | return schema.type; 82 | } 83 | // Return undefined for schemas without explicit type 84 | // This allows the caller to determine the appropriate default behavior 85 | return undefined as any; 86 | } 87 | 88 | // === Re-exported OpenAPI namespace types === 89 | // For cases where specific OpenAPI version types are needed 90 | export { OpenAPIV3, OpenAPIV3_1 }; 91 | -------------------------------------------------------------------------------- /test/enums.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './enums.config.json'; 3 | import enumsSpec from './enums.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = enumsSpec as unknown as OpenAPIObject; 9 | 10 | describe('Test enum generation', () => { 11 | 12 | it('default enum style', () => { 13 | const genDefault = new NgOpenApiGen(spec, { 14 | ...options, 15 | output: 'out/enumStyle/default/' 16 | } as Options); 17 | genDefault.generate(); 18 | const fileContents = fs.readFileSync(fs.realpathSync(`${genDefault.outDir}/models/flavor-enum.ts`)); 19 | expect(/export type FlavorEnum = 'vanilla' | 'StrawBerry' | 'cookie dough' | 'Chocolate Chip' | 'butter_pecan' | 'COKE light';/.test(fileContents.toString())).toBe(true); 20 | }); 21 | 22 | it('enum style "alias"', () => { 23 | const genAlias = new NgOpenApiGen(spec, { 24 | ...options, 25 | output: 'out/enumStyle/alias/', 26 | enumStyle: 'alias' 27 | } as Options); 28 | genAlias.generate(); 29 | const fileContents = fs.readFileSync(fs.realpathSync(`${genAlias.outDir}/models/flavor-enum.ts`)); 30 | expect(/export type FlavorEnum = 'vanilla' | 'StrawBerry' | 'cookie dough' | 'Chocolate Chip' | 'butter_pecan' | 'COKE light';/.test(fileContents.toString())).toBe(true); 31 | }); 32 | 33 | it('enum style "upper"', () => { 34 | const genUpper = new NgOpenApiGen(spec, { 35 | ...options, 36 | output: 'out/enumStyle/upper/', 37 | enumStyle: 'upper' 38 | } as Options); 39 | genUpper.generate(); 40 | const fileContents = fs.readFileSync(fs.realpathSync(`${genUpper.outDir}/models/flavor-enum.ts`)); 41 | expect(/VANILLA = 'vanilla'/.test(fileContents.toString())).toBe(true); 42 | expect(/STRAW_BERRY = 'StrawBerry'/.test(fileContents.toString())).toBe(true); 43 | expect(/COOKIE_DOUGH = 'cookie dough'/.test(fileContents.toString())).toBe(true); 44 | expect(/CHOCOLATE_CHIP = 'Chocolate Chip'/.test(fileContents.toString())).toBe(true); 45 | expect(/BUTTER_PECAN = 'butter_pecan'/.test(fileContents.toString())).toBe(true); 46 | expect(/COKE_LIGHT = 'COKE light'/.test(fileContents.toString())).toBe(true); 47 | }); 48 | 49 | it('enum style "pascal"', () => { 50 | const genPascal = new NgOpenApiGen(spec, { 51 | ...options, 52 | output: 'out/enumStyle/pascal/', 53 | enumStyle: 'pascal' 54 | } as Options); 55 | genPascal.generate(); 56 | const fileContents = fs.readFileSync(fs.realpathSync(`${genPascal.outDir}/models/flavor-enum.ts`)); 57 | expect(/Vanilla = 'vanilla'/.test(fileContents.toString())).toBe(true); 58 | expect(/StrawBerry = 'StrawBerry'/.test(fileContents.toString())).toBe(true); 59 | expect(/CookieDough = 'cookie dough'/.test(fileContents.toString())).toBe(true); 60 | expect(/ChocolateChip = 'Chocolate Chip'/.test(fileContents.toString())).toBe(true); 61 | expect(/ButterPecan = 'butter_pecan'/.test(fileContents.toString())).toBe(true); 62 | expect(/CokeLight = 'COKE light'/.test(fileContents.toString())).toBe(true); 63 | }); 64 | 65 | it('enum style "ignorecase"', () => { 66 | const genIgnorecase = new NgOpenApiGen(spec, { 67 | ...options, 68 | output: 'out/enumStyle/ignorecase/', 69 | enumStyle: 'ignorecase' 70 | } as Options); 71 | genIgnorecase.generate(); 72 | const fileContents = fs.readFileSync(fs.realpathSync(`${genIgnorecase.outDir}/models/flavor-enum.ts`)); 73 | expect(/vanilla = 'vanilla'/.test(fileContents.toString())).toBe(true); 74 | expect(/StrawBerry = 'StrawBerry'/.test(fileContents.toString())).toBe(true); 75 | expect(/cookie_dough = 'cookie dough'/.test(fileContents.toString())).toBe(true); 76 | expect(/Chocolate_Chip = 'Chocolate Chip'/.test(fileContents.toString())).toBe(true); 77 | expect(/butter_pecan = 'butter_pecan'/.test(fileContents.toString())).toBe(true); 78 | expect(/COKE_light = 'COKE light'/.test(fileContents.toString())).toBe(true); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /test/polymorphic.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, TypeAliasDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import options from './polymorphic.config.json'; 4 | import selfRef from './polymorphic.json'; 5 | 6 | const gen = new NgOpenApiGen(selfRef as any, options); 7 | gen.generate(); 8 | 9 | describe('Generation of derived classes using polymorphic.json (as is generated by Swashbuckle)', () => { 10 | it('Tazk model', () => { 11 | const tazk = gen.models.get('Foo.Bar.Tazk'); 12 | const ts = gen.templates.apply('model', tazk); 13 | const parser = new TypescriptParser(); 14 | parser.parseSource(ts).then((ast) => { 15 | expect(ast.declarations.length).toBe(1); 16 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 17 | const decl = ast.declarations[0] as TypeAliasDeclaration; 18 | expect(decl.name).toBe('Tazk'); 19 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 20 | expect(text.replace(/\n/g, ' ')).toContain('Tazk = FooBarTazkBase & { \'taskNumber\'?: number; }'); 21 | 22 | }); 23 | }); 24 | 25 | it('Dooz model', () => { 26 | const tazk = gen.models.get('Foo.Bar.Dooz'); 27 | const ts = gen.templates.apply('model', tazk); 28 | const parser = new TypescriptParser(); 29 | parser.parseSource(ts).then((ast) => { 30 | expect(ast.declarations.length).toBe(1); 31 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 32 | const decl = ast.declarations[0] as InterfaceDeclaration; 33 | expect(decl.name).toBe('Dooz'); 34 | expect(decl.properties).toHaveLength(1); 35 | expect(decl.properties[0].name).toBe('doozObject'); 36 | expect(decl.properties[0].type).toBe('FooBarTazk & {\n\'doozNumber\'?: number;\n}'); 37 | 38 | }); 39 | }); 40 | 41 | it('DiscBase model', () => { 42 | const baze = gen.models.get('Foo.Bar.DiscBase'); 43 | const ts = gen.templates.apply('model', baze); 44 | const parser = new TypescriptParser(); 45 | parser.parseSource(ts).then((ast) => { 46 | expect(ast.declarations.length).toBe(1); 47 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 48 | const decl = ast.declarations[0] as InterfaceDeclaration; 49 | expect(decl.name).toBe('DiscBase'); 50 | expect(decl.properties).toHaveLength(2); 51 | expect(decl.properties[0].name).toBe('$type'); 52 | expect(decl.properties[0].type).toBe('string'); 53 | expect(decl.properties[1].name).toBe('description'); 54 | expect(decl.properties[1].type).toBe('string'); 55 | 56 | }); 57 | }); 58 | 59 | it('DiscOne model', () => { 60 | const one = gen.models.get('Foo.Bar.DiscOne'); 61 | const ts = gen.templates.apply('model', one); 62 | const parser = new TypescriptParser(); 63 | parser.parseSource(ts).then((ast) => { 64 | expect(ast.declarations.length).toBe(1); 65 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 66 | const decl = ast.declarations[0] as TypeAliasDeclaration; 67 | expect(decl.name).toBe('DiscOne'); 68 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 69 | expect(text.replace(/\n/g, ' ')).toContain('DiscOne = FooBarDiscBase & { \'$type\': \'disc-1\'; \'discNumber\'?: number; }'); 70 | 71 | }); 72 | }); 73 | 74 | it('DiscTwo model', () => { 75 | const two = gen.models.get('Foo.Bar.DiscTwo'); 76 | const ts = gen.templates.apply('model', two); 77 | const parser = new TypescriptParser(); 78 | parser.parseSource(ts).then((ast) => { 79 | expect(ast.declarations.length).toBe(1); 80 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 81 | const decl = ast.declarations[0] as TypeAliasDeclaration; 82 | expect(decl.name).toBe('DiscTwo'); 83 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 84 | expect(text.replace(/\n/g, ' ')).toContain('DiscTwo = FooBarDiscBase & { \'$type\': \'disc-2\'; \'discText\'?: string; }'); 85 | 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/cbor-duplicate-methods.spec.ts: -------------------------------------------------------------------------------- 1 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 2 | import options from './cbor-duplicate-methods.config.json'; 3 | import cborSpec from './cbor-duplicate-methods.json'; 4 | import * as fs from 'fs'; 5 | import { Options } from '../lib/options'; 6 | import { OpenAPIObject } from '../lib/openapi-typings'; 7 | 8 | const spec = cborSpec as unknown as OpenAPIObject; 9 | 10 | describe('Test CBOR duplicate methods issue', () => { 11 | 12 | it('should not generate duplicate methods when using application/cbor for both request and response', () => { 13 | const gen = new NgOpenApiGen(spec, { 14 | ...options, 15 | output: 'out/cbor-duplicate-methods/' 16 | } as Options); 17 | gen.generate(); 18 | 19 | // Check that services are enabled 20 | expect(options.services).toBe(true); 21 | 22 | // Get the searchPost operation which uses application/cbor for both request and response 23 | const operation = gen.operations.get('searchPost'); 24 | expect(operation).toBeDefined(); 25 | if (!operation) return; 26 | 27 | expect(operation.path).toBe('/search'); 28 | expect(operation.method).toBe('post'); 29 | 30 | // Check request body 31 | expect(operation.requestBody).toBeDefined(); 32 | if (operation.requestBody) { 33 | expect(operation.requestBody.content.length).toBe(1); 34 | expect(operation.requestBody.content[0].mediaType).toBe('application/cbor'); 35 | } 36 | 37 | // Check success response 38 | expect(operation.successResponse).toBeDefined(); 39 | if (operation.successResponse) { 40 | expect(operation.successResponse.content.length).toBe(1); 41 | expect(operation.successResponse.content[0].mediaType).toBe('application/cbor'); 42 | } 43 | 44 | // The key test: check that only one variant is generated, not duplicates 45 | expect(operation.variants).toBeDefined(); 46 | expect(operation.variants.length).toBe(1); 47 | 48 | // Check the variant method name 49 | const variant = operation.variants[0]; 50 | expect(variant.methodName).toBe('searchPost'); 51 | 52 | // Check that the generated service file contains the correct method 53 | const serviceFileContent = fs.readFileSync(fs.realpathSync(`${gen.outDir}/services/api.service.ts`), 'utf8'); 54 | 55 | // Should contain exactly one searchPost method variant 56 | const searchPostMethods = serviceFileContent.match(/^\s+searchPost[^(]*\(/gm); 57 | expect(searchPostMethods).toBeDefined(); 58 | if (searchPostMethods) { 59 | // Should have exactly 2 methods: searchPost and searchPost$Response 60 | // Since there's only one content type variant (single CBOR), no content type suffix should be added 61 | expect(searchPostMethods.length).toBe(2); 62 | expect(serviceFileContent).toContain('searchPost('); 63 | expect(serviceFileContent).toContain('searchPost$Response('); 64 | // Should NOT contain methods with content type suffix since there's only one variant 65 | expect(serviceFileContent).not.toContain('searchPost$Cbor$Cbor('); 66 | } 67 | 68 | // Should NOT contain duplicate method declarations with the same signature 69 | const methodDeclarations = serviceFileContent.match(/^\s+searchPost\(/gm); 70 | expect(methodDeclarations?.length).toBe(1); 71 | const responseMethodDeclarations = serviceFileContent.match(/^\s+searchPost\$Response\(/gm); 72 | expect(responseMethodDeclarations?.length).toBe(1); 73 | }); 74 | 75 | it('should generate proper variants for mixed content operation', () => { 76 | const gen = new NgOpenApiGen(spec, { 77 | ...options, 78 | output: 'out/cbor-duplicate-methods/' 79 | } as Options); 80 | gen.generate(); 81 | 82 | const operation = gen.operations.get('mixedContent'); 83 | expect(operation).toBeDefined(); 84 | if (!operation) return; 85 | 86 | expect(operation.path).toBe('/mixed-content'); 87 | expect(operation.method).toBe('post'); 88 | 89 | // Should have 4 variants: Json-Json, Json-Cbor, Cbor-Json, Cbor-Cbor 90 | expect(operation.variants.length).toBe(4); 91 | 92 | const methodNames = operation.variants.map(v => v.methodName).sort(); 93 | expect(methodNames).toEqual([ 94 | 'mixedContent$Cbor$Cbor', 95 | 'mixedContent$Cbor$Json', 96 | 'mixedContent$Json$Cbor', 97 | 'mixedContent$Json$Json' 98 | ]); 99 | 100 | // Each variant should be unique 101 | const uniqueMethodNames = new Set(methodNames); 102 | expect(uniqueMethodNames.size).toBe(4); 103 | }); 104 | 105 | }); 106 | -------------------------------------------------------------------------------- /lib/cmd-args.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentParser } from 'argparse'; 2 | import pkg from '../package.json'; 3 | import schema from '../ng-openapi-gen-schema.json'; 4 | import { Options } from './options.js'; 5 | import fs from 'fs'; 6 | import { kebabCase } from 'lodash'; 7 | 8 | const MNEMONICS: { [key: string]: string } = { 'input': 'i', 'output': 'o' }; 9 | const DEFAULT = 'ng-openapi-gen.json'; 10 | 11 | function createParser() { 12 | const argParser = new ArgumentParser({ 13 | // eslint-disable-next-line @typescript-eslint/naming-convention 14 | add_help: true, 15 | description: ` 16 | Generator for API clients described with OpenAPI 3.0 / 3.1 specification for 17 | Angular 16+ projects. Requires a configuration file, which defaults to 18 | ${DEFAULT} in the current directory. The file can also be 19 | specified using '--config ' or '-c '. 20 | All settings in the configuration file can be overriding by setting the 21 | corresponding argument in the command-line. For example, to specify a 22 | custom suffix for service classes via command-line, pass the command-line 23 | argument '--serviceSuffix Suffix'. Kebab-case is also accepted, so, the same 24 | argument could be set as '--service-suffix Suffix' 25 | As the only required argument is the input for OpenAPI specification, 26 | a configuration file is only required if no --input argument is set.`.trim() 27 | }); 28 | argParser.add_argument( 29 | '-v', 30 | '--version', 31 | { 32 | action: 'version', 33 | version: pkg.version 34 | } 35 | ); 36 | argParser.add_argument( 37 | '-c', 38 | '--config', 39 | { 40 | help: ` 41 | The configuration file to be used. If not specified, assumes that 42 | ${DEFAULT} in the current directory`.trim(), 43 | dest: 'config', 44 | default: `./${DEFAULT}` 45 | } 46 | ); 47 | const props = schema.properties; 48 | for (const key of Object.keys(props)) { 49 | if (key === '$schema') { 50 | continue; 51 | } 52 | const kebab = kebabCase(key); 53 | const desc = (props as any)[key]; 54 | const names = []; 55 | const mnemonic = MNEMONICS[key]; 56 | if (mnemonic) { 57 | names.push('-' + mnemonic); 58 | } 59 | names.push('--' + key); 60 | if (kebab !== key) { 61 | names.push('--' + kebab); 62 | } 63 | argParser.add_argument(...(names as [string]), { 64 | help: desc.description, 65 | dest: key 66 | }); 67 | } 68 | return argParser; 69 | } 70 | 71 | /** 72 | * Parses the options from command-line arguments 73 | */ 74 | export function parseOptions(sysArgs?: string[]): Options { 75 | const argParser = createParser(); 76 | const args = argParser.parse_args(sysArgs); 77 | let options: any = {}; 78 | if (args.config) { 79 | if (fs.existsSync(args.config)) { 80 | options = JSON.parse(fs.readFileSync(args.config, { encoding: 'utf-8' })); 81 | } else if (args.config === `./${DEFAULT}`) { 82 | if ((args.input || '').length === 0) { 83 | throw new Error(`No input is given, and the file ${DEFAULT} doesn't exist. 84 | For help, run ng-openapi-gen --help`); 85 | } 86 | } else { 87 | throw new Error(`The given configuration file doesn't exist: ${args.config}.`); 88 | } 89 | } 90 | objectifyCustomizedResponseType(args); 91 | 92 | const props = schema.properties; 93 | 94 | for (const key of Object.keys(args)) { 95 | let value = args[key]; 96 | if (key === 'config' || value == null) { 97 | // This is the only option that is not from the configuration itself, or not passed in 98 | continue; 99 | } 100 | const desc = (props as any)[key]; 101 | if (desc.type === 'array') { 102 | value = (value || '').trim().split(',').map((v: string) => v.trim()); 103 | } else if (value === 'true') { 104 | value = true; 105 | } else if (value === 'false') { 106 | value = false; 107 | } 108 | if (desc.type === 'number' && typeof value === 'string') { 109 | value = parseInt(value, 10); 110 | } 111 | if (value !== undefined) { 112 | options[key] = value; 113 | } 114 | } 115 | if (options.input === undefined || options.input === '') { 116 | throw new Error('No input (OpenAPI specification) defined'); 117 | } 118 | return options; 119 | } 120 | 121 | function objectifyCustomizedResponseType(args: { customizedResponseType?: string }): void { 122 | 123 | if (!args.customizedResponseType) return; 124 | 125 | try { 126 | args.customizedResponseType = JSON.parse(args.customizedResponseType); 127 | } catch { 128 | throw new Error(`Invalid JSON string: [${args.customizedResponseType}] \n for --customizedResponseType`); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /test/openapi31-nullable.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "OpenAPI 3.1 Nullable Types Test", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/nullable-array": { 9 | "get": { 10 | "operationId": "getNullableArray", 11 | "responses": { 12 | "200": { 13 | "description": "Success", 14 | "content": { 15 | "application/json": { 16 | "schema": { 17 | "$ref": "#/components/schemas/NullableArrayResponse" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | "/mixed-types": { 26 | "post": { 27 | "operationId": "postMixedTypes", 28 | "requestBody": { 29 | "content": { 30 | "application/json": { 31 | "schema": { 32 | "$ref": "#/components/schemas/MixedTypesInput" 33 | } 34 | } 35 | } 36 | }, 37 | "responses": { 38 | "200": { 39 | "description": "Success", 40 | "content": { 41 | "application/json": { 42 | "schema": { 43 | "$ref": "#/components/schemas/MixedTypesOutput" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | }, 51 | "/jsonschema-types": { 52 | "get": { 53 | "operationId": "getJsonSchemaTypes", 54 | "responses": { 55 | "200": { 56 | "description": "JSON Schema types example", 57 | "content": { 58 | "application/json": { 59 | "schema": { 60 | "$ref": "#/components/schemas/JsonSchemaTypes" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | "components": { 70 | "schemas": { 71 | "NullableArrayResponse": { 72 | "type": "object", 73 | "properties": { 74 | "items": { 75 | "type": ["array", "null"], 76 | "items": { 77 | "type": "string" 78 | }, 79 | "description": "Array that can be null in OpenAPI 3.1" 80 | }, 81 | "nullableString": { 82 | "type": ["string", "null"], 83 | "description": "String that can be null" 84 | }, 85 | "optionalArray": { 86 | "type": "array", 87 | "items": { 88 | "type": ["string", "null"] 89 | }, 90 | "description": "Array of nullable strings" 91 | } 92 | } 93 | }, 94 | "MixedTypesInput": { 95 | "type": "object", 96 | "properties": { 97 | "unionType": { 98 | "type": ["string", "number", "boolean"], 99 | "description": "OpenAPI 3.1 union type" 100 | }, 101 | "constValue": { 102 | "const": "fixed_value", 103 | "description": "Const value in OpenAPI 3.1" 104 | }, 105 | "enumWithNull": { 106 | "type": ["string", "null"], 107 | "enum": ["option1", "option2", null], 108 | "description": "Enum that includes null" 109 | } 110 | } 111 | }, 112 | "MixedTypesOutput": { 113 | "type": "object", 114 | "properties": { 115 | "result": { 116 | "type": ["string", "number"], 117 | "description": "Result can be string or number" 118 | }, 119 | "status": { 120 | "const": "success", 121 | "description": "Always success" 122 | }, 123 | "data": { 124 | "type": ["object", "array", "null"], 125 | "description": "Flexible data field" 126 | } 127 | } 128 | }, 129 | "JsonSchemaTypes": { 130 | "type": "object", 131 | "properties": { 132 | "unionField": { 133 | "type": ["string", "number", "boolean"], 134 | "description": "Union of primitive types" 135 | }, 136 | "nullableUnion": { 137 | "type": ["string", "number", "null"], 138 | "description": "Union that includes null" 139 | }, 140 | "constField": { 141 | "const": "constant_value", 142 | "description": "Constant field" 143 | }, 144 | "enumWithTypes": { 145 | "type": ["string", "number"], 146 | "enum": ["text", 42, "other"], 147 | "description": "Enum with mixed types" 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /test/openapi31-jsonschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "OpenAPI 3.1 JSON Schema Features Test", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/schema-features": { 9 | "post": { 10 | "operationId": "testSchemaFeatures", 11 | "requestBody": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "$ref": "#/components/schemas/AdvancedSchemaFeatures" 16 | } 17 | } 18 | } 19 | }, 20 | "responses": { 21 | "200": { 22 | "description": "Success", 23 | "content": { 24 | "application/json": { 25 | "schema": { 26 | "$ref": "#/components/schemas/ResponseWithDiscriminator" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "/array-contains": { 35 | "get": { 36 | "operationId": "getArrayWithContains", 37 | "responses": { 38 | "200": { 39 | "description": "Array with contains example", 40 | "content": { 41 | "application/json": { 42 | "schema": { 43 | "$ref": "#/components/schemas/ArrayWithContains" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "components": { 53 | "schemas": { 54 | "AdvancedSchemaFeatures": { 55 | "type": "object", 56 | "properties": { 57 | "tupleArray": { 58 | "type": "array", 59 | "prefixItems": [ 60 | {"type": "string"}, 61 | {"type": "number"}, 62 | {"type": "boolean"} 63 | ], 64 | "items": false, 65 | "description": "Tuple with prefixItems (OpenAPI 3.1 feature)" 66 | }, 67 | "conditionalProperty": { 68 | "type": "object", 69 | "properties": { 70 | "type": {"type": "string"}, 71 | "value": {} 72 | }, 73 | "if": { 74 | "properties": {"type": {"const": "number"}} 75 | }, 76 | "then": { 77 | "properties": {"value": {"type": "number"}} 78 | }, 79 | "else": { 80 | "properties": {"value": {"type": "string"}} 81 | } 82 | } 83 | } 84 | }, 85 | "Dog": { 86 | "type": "object", 87 | "properties": { 88 | "pet_type": { 89 | "const": "dog" 90 | }, 91 | "breed": { 92 | "type": "string", 93 | "enum": ["Dingo", "Husky", "Retriever", "Shepherd"] 94 | }, 95 | "bark": { 96 | "type": "boolean" 97 | } 98 | }, 99 | "required": ["pet_type", "breed"] 100 | }, 101 | "Cat": { 102 | "type": "object", 103 | "properties": { 104 | "pet_type": { 105 | "const": "cat" 106 | }, 107 | "hunts": { 108 | "type": "boolean" 109 | }, 110 | "age": { 111 | "type": "integer", 112 | "minimum": 0 113 | } 114 | }, 115 | "required": ["pet_type"] 116 | }, 117 | "ResponseWithDiscriminator": { 118 | "oneOf": [ 119 | {"$ref": "#/components/schemas/Dog"}, 120 | {"$ref": "#/components/schemas/Cat"} 121 | ], 122 | "discriminator": { 123 | "propertyName": "pet_type", 124 | "mapping": { 125 | "dog": "#/components/schemas/Dog", 126 | "cat": "#/components/schemas/Cat" 127 | } 128 | } 129 | }, 130 | "ArrayWithContains": { 131 | "type": "object", 132 | "properties": { 133 | "mixedArray": { 134 | "type": "array", 135 | "contains": { 136 | "type": "string", 137 | "pattern": "^special" 138 | }, 139 | "items": { 140 | "type": ["string", "number"] 141 | }, 142 | "description": "Array that must contain at least one string starting with 'special'" 143 | }, 144 | "dependentField": { 145 | "type": "object", 146 | "properties": { 147 | "hasDiscount": {"type": "boolean"}, 148 | "discountPercent": {"type": "number"} 149 | }, 150 | "dependentRequired": { 151 | "hasDiscount": ["discountPercent"] 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /test/petstore-3.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "MIT" 8 | } 9 | }, 10 | "servers": [ 11 | { 12 | "url": "http://petstore.swagger.io/v1" 13 | } 14 | ], 15 | "paths": { 16 | "/pets": { 17 | "get": { 18 | "summary": "List all pets", 19 | "operationId": "listPets", 20 | "tags": [ 21 | "pets" 22 | ], 23 | "parameters": [ 24 | { 25 | "name": "limit", 26 | "in": "query", 27 | "description": "How many items to return at one time (max 100)", 28 | "required": false, 29 | "schema": { 30 | "type": "integer", 31 | "format": "int32" 32 | } 33 | } 34 | ], 35 | "responses": { 36 | "200": { 37 | "description": "A paged array of pets", 38 | "headers": { 39 | "x-next": { 40 | "description": "A link to the next page of responses", 41 | "schema": { 42 | "type": "string" 43 | } 44 | } 45 | }, 46 | "content": { 47 | "application/json": { 48 | "schema": { 49 | "$ref": "#/components/schemas/Pets" 50 | } 51 | } 52 | } 53 | }, 54 | "default": { 55 | "description": "unexpected error", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/Error" 60 | } 61 | } 62 | } 63 | } 64 | } 65 | }, 66 | "post": { 67 | "summary": "Create a pet", 68 | "operationId": "createPets", 69 | "tags": [ 70 | "pets" 71 | ], 72 | "responses": { 73 | "201": { 74 | "description": "Null response" 75 | }, 76 | "default": { 77 | "description": "unexpected error", 78 | "content": { 79 | "application/json": { 80 | "schema": { 81 | "$ref": "#/components/schemas/Error" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "/pets/{petId}": { 90 | "get": { 91 | "summary": "Info for a specific pet", 92 | "operationId": "showPetById", 93 | "tags": [ 94 | "pets" 95 | ], 96 | "parameters": [ 97 | { 98 | "name": "petId", 99 | "in": "path", 100 | "required": true, 101 | "description": "Pet's id to retrieve", 102 | "schema": { 103 | "type": "string" 104 | } 105 | } 106 | ], 107 | "responses": { 108 | "200": { 109 | "description": "Expected response to a valid request", 110 | "content": { 111 | "application/json": { 112 | "schema": { 113 | "$ref": "#/components/schemas/Pets" 114 | } 115 | } 116 | } 117 | }, 118 | "default": { 119 | "description": "unexpected error", 120 | "content": { 121 | "application/json": { 122 | "schema": { 123 | "$ref": "#/components/schemas/Error" 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "components": { 133 | "schemas": { 134 | "Pet": { 135 | "required": [ 136 | "id", 137 | "name" 138 | ], 139 | "properties": { 140 | "id": { 141 | "type": "integer", 142 | "format": "int64" 143 | }, 144 | "name": { 145 | "type": "string" 146 | }, 147 | "tag": { 148 | "type": "string" 149 | } 150 | } 151 | }, 152 | "Pets": { 153 | "type": "array", 154 | "items": { 155 | "$ref": "#/components/schemas/Pet" 156 | } 157 | }, 158 | "Error": { 159 | "required": [ 160 | "code", 161 | "message" 162 | ], 163 | "properties": { 164 | "code": { 165 | "type": "integer", 166 | "format": "int32" 167 | }, 168 | "message": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/petstore-3.1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "Swagger Petstore", 6 | "license": { 7 | "name": "MIT", 8 | "url": "https://opensource.org/licenses/MIT" 9 | } 10 | }, 11 | "servers": [ 12 | { 13 | "url": "http://petstore.swagger.io/v1" 14 | } 15 | ], 16 | "paths": { 17 | "/pets": { 18 | "get": { 19 | "summary": "List all pets", 20 | "operationId": "listPets", 21 | "tags": [ 22 | "pets" 23 | ], 24 | "parameters": [ 25 | { 26 | "name": "limit", 27 | "in": "query", 28 | "description": "How many items to return at one time (max 100)", 29 | "required": false, 30 | "schema": { 31 | "type": "integer", 32 | "format": "int32" 33 | } 34 | } 35 | ], 36 | "responses": { 37 | "200": { 38 | "description": "A paged array of pets", 39 | "headers": { 40 | "x-next": { 41 | "description": "A link to the next page of responses", 42 | "schema": { 43 | "type": "string" 44 | } 45 | } 46 | }, 47 | "content": { 48 | "application/json": { 49 | "schema": { 50 | "$ref": "#/components/schemas/Pets" 51 | } 52 | } 53 | } 54 | }, 55 | "default": { 56 | "description": "unexpected error", 57 | "content": { 58 | "application/json": { 59 | "schema": { 60 | "$ref": "#/components/schemas/Error" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "post": { 68 | "summary": "Create a pet", 69 | "operationId": "createPets", 70 | "tags": [ 71 | "pets" 72 | ], 73 | "responses": { 74 | "201": { 75 | "description": "Null response" 76 | }, 77 | "default": { 78 | "description": "unexpected error", 79 | "content": { 80 | "application/json": { 81 | "schema": { 82 | "$ref": "#/components/schemas/Error" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | }, 90 | "/pets/{petId}": { 91 | "get": { 92 | "summary": "Info for a specific pet", 93 | "operationId": "showPetById", 94 | "tags": [ 95 | "pets" 96 | ], 97 | "parameters": [ 98 | { 99 | "name": "petId", 100 | "in": "path", 101 | "required": true, 102 | "description": "The id of the pet to retrieve", 103 | "schema": { 104 | "type": "string" 105 | } 106 | } 107 | ], 108 | "responses": { 109 | "200": { 110 | "description": "Expected response to a valid request", 111 | "content": { 112 | "application/json": { 113 | "schema": { 114 | "$ref": "#/components/schemas/Pet" 115 | } 116 | } 117 | } 118 | }, 119 | "default": { 120 | "description": "unexpected error", 121 | "content": { 122 | "application/json": { 123 | "schema": { 124 | "$ref": "#/components/schemas/Error" 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | }, 133 | "components": { 134 | "schemas": { 135 | "Pet": { 136 | "type": "object", 137 | "required": [ 138 | "id", 139 | "name" 140 | ], 141 | "properties": { 142 | "id": { 143 | "type": "integer", 144 | "format": "int64" 145 | }, 146 | "name": { 147 | "type": "string" 148 | }, 149 | "tag": { 150 | "type": "string" 151 | } 152 | } 153 | }, 154 | "Pets": { 155 | "type": "array", 156 | "items": { 157 | "$ref": "#/components/schemas/Pet" 158 | } 159 | }, 160 | "Error": { 161 | "type": "object", 162 | "required": [ 163 | "code", 164 | "message" 165 | ], 166 | "properties": { 167 | "code": { 168 | "type": "integer", 169 | "format": "int32" 170 | }, 171 | "message": { 172 | "type": "string" 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /test/petstore-3.0.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, TypeAliasDeclaration, TypescriptParser, ClassDeclaration } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import options from './petstore-3.0.config.json'; 4 | import petstoreSpec from './petstore-3.0.json'; 5 | import { OpenAPIObject } from '../lib/openapi-typings'; 6 | 7 | const spec = petstoreSpec as OpenAPIObject; 8 | const gen = new NgOpenApiGen(spec, options); 9 | gen.generate(); 10 | 11 | describe('Generation tests using petstore-3.0.json', () => { 12 | 13 | it('Tags', () => { 14 | expect(gen.services.size).toBe(1); 15 | }); 16 | 17 | it('pets tag', () => { 18 | const pets = gen.services.get('pets'); 19 | expect(pets).toBeDefined(); 20 | if (!pets) return; 21 | expect(pets.operations.length).toBe(3); 22 | const ts = gen.templates.apply('service', pets); 23 | const parser = new TypescriptParser(); 24 | parser.parseSource(ts).then(ast => { 25 | expect(ast.declarations.length).toBe(1); 26 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 27 | const cls = ast.declarations[0] as ClassDeclaration; 28 | 29 | const listPets = cls.methods.find(m => m.name === 'listPets'); 30 | expect(listPets).toBeDefined(); 31 | if (listPets) { 32 | expect(listPets.parameters.length).toBe(2); 33 | const type = listPets.parameters[0].type; 34 | expect(type).toEqual('ListPets$Params'); 35 | } 36 | 37 | const createPets = cls.methods.find(m => m.name === 'createPets'); 38 | expect(createPets).toBeDefined(); 39 | if (createPets) { 40 | expect(createPets.parameters.length).toBe(2); 41 | const type = createPets.parameters[0].type; 42 | expect(type).toEqual('CreatePets$Params'); 43 | } 44 | 45 | const showPetById = cls.methods.find(m => m.name === 'showPetById'); 46 | expect(showPetById).toBeDefined(); 47 | if (showPetById) { 48 | expect(showPetById.parameters.length).toBe(2); 49 | const type = showPetById.parameters[0].type; 50 | expect(type).toEqual('ShowPetById$Params'); 51 | } 52 | 53 | 54 | }); 55 | }); 56 | 57 | it('Pet model', () => { 58 | const pet = gen.models.get('Pet'); 59 | const ts = gen.templates.apply('model', pet); 60 | const parser = new TypescriptParser(); 61 | parser.parseSource(ts).then(ast => { 62 | expect(ast.declarations.length).toBe(1); 63 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 64 | const decl = ast.declarations[0] as InterfaceDeclaration; 65 | expect(decl.name).toBe('PetstorePetModel'); 66 | expect(decl.properties.length).toBe(3); 67 | const id = decl.properties[0]; 68 | expect(id.isOptional).toBe(false); 69 | expect(id.name).toBe('id'); 70 | expect(id.type).toBe('number'); 71 | const name = decl.properties[1]; 72 | expect(name.isOptional).toBe(false); 73 | expect(name.name).toBe('name'); 74 | expect(name.type).toBe('string'); 75 | const tag = decl.properties[2]; 76 | expect(tag.isOptional).toBe(true); 77 | expect(tag.name).toBe('tag'); 78 | expect(tag.type).toBe('string'); 79 | 80 | }); 81 | }); 82 | 83 | it('Pets model', () => { 84 | const pets = gen.models.get('Pets'); 85 | const ts = gen.templates.apply('model', pets); 86 | const parser = new TypescriptParser(); 87 | parser.parseSource(ts).then(ast => { 88 | expect(ast.imports.find(i => i.libraryName.endsWith('/petstore-pet-model'))).toBeDefined(); 89 | expect(ast.declarations.length).toBe(1); 90 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 91 | const decl = ast.declarations[0] as TypeAliasDeclaration; 92 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 93 | expect(text).toBe('export type PetstorePetsModel = Array;'); 94 | 95 | }); 96 | }); 97 | 98 | it('Error model', () => { 99 | const entity = gen.models.get('Error'); 100 | const ts = gen.templates.apply('model', entity); 101 | const parser = new TypescriptParser(); 102 | parser.parseSource(ts).then(ast => { 103 | expect(ast.declarations.length).toBe(1); 104 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 105 | const decl = ast.declarations[0] as InterfaceDeclaration; 106 | expect(decl.name).toBe('PetstoreErrorModel'); 107 | expect(decl.properties.length).toBe(2); 108 | const code = decl.properties[0]; 109 | expect(code.isOptional).toBe(false); 110 | expect(code.name).toBe('code'); 111 | expect(code.type).toBe('number'); 112 | const message = decl.properties[1]; 113 | expect(message.isOptional).toBe(false); 114 | expect(message.name).toBe('message'); 115 | expect(message.type).toBe('string'); 116 | 117 | }); 118 | }); 119 | 120 | }); 121 | -------------------------------------------------------------------------------- /test/petstore-3.1.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, TypeAliasDeclaration, TypescriptParser, ClassDeclaration } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import options from './petstore-3.1.config.json'; 4 | import petstoreSpec from './petstore-3.1.json'; 5 | import { OpenAPIObject } from '../lib/openapi-typings'; 6 | 7 | const spec = petstoreSpec as OpenAPIObject; 8 | const gen = new NgOpenApiGen(spec, options); 9 | gen.generate(); 10 | 11 | 12 | describe('Generation tests using petstore-3.1.json', () => { 13 | 14 | it('Tags', () => { 15 | expect(gen.services.size).toBe(1); 16 | }); 17 | 18 | it('pets tag', () => { 19 | const pets = gen.services.get('pets'); 20 | expect(pets).toBeDefined(); 21 | if (!pets) return; 22 | expect(pets.operations.length).toBe(3); 23 | const ts = gen.templates.apply('service', pets); 24 | const parser = new TypescriptParser(); 25 | parser.parseSource(ts).then(ast => { 26 | expect(ast.declarations.length).toBe(1); 27 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 28 | const cls = ast.declarations[0] as ClassDeclaration; 29 | 30 | const listPets = cls.methods.find(m => m.name === 'listPets'); 31 | expect(listPets).toBeDefined(); 32 | if (listPets) { 33 | expect(listPets.parameters.length).toBe(2); 34 | const type = listPets.parameters[0].type; 35 | expect(type).toEqual('ListPets$Params'); 36 | } 37 | 38 | const createPets = cls.methods.find(m => m.name === 'createPets'); 39 | expect(createPets).toBeDefined(); 40 | if (createPets) { 41 | expect(createPets.parameters.length).toBe(2); 42 | const type = createPets.parameters[0].type; 43 | expect(type).toEqual('CreatePets$Params'); 44 | } 45 | 46 | const showPetById = cls.methods.find(m => m.name === 'showPetById'); 47 | expect(showPetById).toBeDefined(); 48 | if (showPetById) { 49 | expect(showPetById.parameters.length).toBe(2); 50 | const type = showPetById.parameters[0].type; 51 | expect(type).toEqual('ShowPetById$Params'); 52 | } 53 | 54 | 55 | }); 56 | }); 57 | 58 | it('Pet model', () => { 59 | const pet = gen.models.get('Pet'); 60 | const ts = gen.templates.apply('model', pet); 61 | const parser = new TypescriptParser(); 62 | parser.parseSource(ts).then(ast => { 63 | expect(ast.declarations.length).toBe(1); 64 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 65 | const decl = ast.declarations[0] as InterfaceDeclaration; 66 | expect(decl.name).toBe('PetstorePetModel'); 67 | expect(decl.properties.length).toBe(3); 68 | const id = decl.properties[0]; 69 | expect(id.isOptional).toBe(false); 70 | expect(id.name).toBe('id'); 71 | expect(id.type).toBe('number'); 72 | const name = decl.properties[1]; 73 | expect(name.isOptional).toBe(false); 74 | expect(name.name).toBe('name'); 75 | expect(name.type).toBe('string'); 76 | const tag = decl.properties[2]; 77 | expect(tag.isOptional).toBe(true); 78 | expect(tag.name).toBe('tag'); 79 | expect(tag.type).toBe('string'); 80 | 81 | }); 82 | }); 83 | 84 | it('Pets model', () => { 85 | const pets = gen.models.get('Pets'); 86 | const ts = gen.templates.apply('model', pets); 87 | const parser = new TypescriptParser(); 88 | parser.parseSource(ts).then(ast => { 89 | expect(ast.imports.find(i => i.libraryName.endsWith('/petstore-pet-model'))).toBeDefined(); 90 | expect(ast.declarations.length).toBe(1); 91 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 92 | const decl = ast.declarations[0] as TypeAliasDeclaration; 93 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 94 | expect(text).toBe('export type PetstorePetsModel = Array;'); 95 | 96 | }); 97 | }); 98 | 99 | it('Error model', () => { 100 | const entity = gen.models.get('Error'); 101 | const ts = gen.templates.apply('model', entity); 102 | const parser = new TypescriptParser(); 103 | parser.parseSource(ts).then(ast => { 104 | expect(ast.declarations.length).toBe(1); 105 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 106 | const decl = ast.declarations[0] as InterfaceDeclaration; 107 | expect(decl.name).toBe('PetstoreErrorModel'); 108 | expect(decl.properties.length).toBe(2); 109 | const code = decl.properties[0]; 110 | expect(code.isOptional).toBe(false); 111 | expect(code.name).toBe('code'); 112 | expect(code.type).toBe('number'); 113 | const message = decl.properties[1]; 114 | expect(message.isOptional).toBe(false); 115 | expect(message.name).toBe('message'); 116 | expect(message.type).toBe('string'); 117 | 118 | }); 119 | }); 120 | 121 | }); 122 | -------------------------------------------------------------------------------- /test/person-place.spec.ts: -------------------------------------------------------------------------------- 1 | import { InterfaceDeclaration, TypeAliasDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import options from './person-place.config.json'; 4 | import personAndPlaceSpec from './person-place.json'; 5 | 6 | const gen = new NgOpenApiGen(personAndPlaceSpec as any, options); 7 | gen.generate(); 8 | 9 | describe('Generation tests using person-place.json', () => { 10 | it('Id model', () => { 11 | const id = gen.models.get('Id'); 12 | const ts = gen.templates.apply('model', id); 13 | const parser = new TypescriptParser(); 14 | parser.parseSource(ts).then(ast => { 15 | expect(ast.declarations.length).toBe(1); 16 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 17 | const decl = ast.declarations[0] as TypeAliasDeclaration; 18 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 19 | expect(text).toBe('export type PPIdModel = string;'); 20 | 21 | }); 22 | }); 23 | 24 | it('Entity model', () => { 25 | const entity = gen.models.get('Entity'); 26 | const ts = gen.templates.apply('model', entity); 27 | const parser = new TypescriptParser(); 28 | parser.parseSource(ts).then(ast => { 29 | expect(ast.imports.find(i => i.libraryName.endsWith('/pp-id-model'))).toBeDefined(); 30 | expect(ast.declarations.length).toBe(1); 31 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 32 | const decl = ast.declarations[0] as InterfaceDeclaration; 33 | expect(decl.name).toBe('PPEntityModel'); 34 | expect(decl.properties.length).toBe(1); 35 | const id = decl.properties[0]; 36 | expect(id.name).toBe('id'); 37 | expect(id.type).toBe('PPIdModel'); 38 | 39 | }); 40 | }); 41 | 42 | it('Person model', () => { 43 | const person = gen.models.get('Person'); 44 | const ts = gen.templates.apply('model', person); 45 | const parser = new TypescriptParser(); 46 | parser.parseSource(ts).then(ast => { 47 | expect(ast.imports.find(i => i.libraryName.endsWith('/pp-entity-model'))).toBeDefined(); 48 | expect(ast.imports.find(i => i.libraryName.endsWith('/pp-person-place-model'))).toBeDefined(); 49 | expect(ast.declarations.length).toBe(1); 50 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 51 | const decl = ast.declarations[0] as TypeAliasDeclaration; 52 | expect(decl.name).toBe('PPPersonModel'); 53 | // There's no support for additional properties in typescript-parser. Check as text. 54 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 55 | expect(text).toContain('PPEntityModel & {\n\'name\'?: string;\n\'places\'?: Array;\n}'); 56 | 57 | }); 58 | }); 59 | 60 | it('Place model', () => { 61 | const place = gen.models.get('Place'); 62 | const ts = gen.templates.apply('model', place); 63 | const parser = new TypescriptParser(); 64 | parser.parseSource(ts).then(ast => { 65 | expect(ast.imports.find(i => i.libraryName.endsWith('/pp-entity-model'))).toBeDefined(); 66 | expect(ast.declarations.length).toBe(1); 67 | expect(ast.declarations[0]).toEqual(expect.any(TypeAliasDeclaration)); 68 | const decl = ast.declarations[0] as TypeAliasDeclaration; 69 | expect(decl.name).toBe('PPPlaceModel'); 70 | // There's no support for additional properties in typescript-parser. Check as text. 71 | const text = ts.substring(decl.start || 0, decl.end || ts.length); 72 | expect(text).toContain('PPEntityModel & (PPGpsLocationModel | {\n\n/**\n * Street address\n */\n\'address\'?: string;\n}) & {\n\'description\'\?: string;\n[key: string]: string;\n}'); 73 | 74 | }); 75 | }); 76 | 77 | it('PersonPlace model', () => { 78 | const person = gen.models.get('PersonPlace'); 79 | const ts = gen.templates.apply('model', person); 80 | const parser = new TypescriptParser(); 81 | parser.parseSource(ts).then(ast => { 82 | expect(ast.imports.find(i => i.libraryName.endsWith('/pp-place-model'))).toBeDefined(); 83 | expect(ast.declarations.length).toBe(1); 84 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 85 | const decl = ast.declarations[0] as InterfaceDeclaration; 86 | expect(decl.name).toBe('PPPersonPlaceModel'); 87 | expect(decl.properties.length).toBe(2); 88 | const since = decl.properties.find(p => p.name === 'since'); 89 | expect(since).toBeDefined(); 90 | if (since) { 91 | expect(since.type).toBe('string'); 92 | // The parser doesn't hold comments 93 | expect(ts).toContain('* The date this place was assigned to the person'); 94 | } 95 | const place = decl.properties.find(p => p.name === 'place'); 96 | expect(place).toBeDefined(); 97 | if (place) { 98 | expect(place.type).toBe('PPPlaceModel'); 99 | } 100 | 101 | }); 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /test/openapi31-webhooks.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, InterfaceDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import options from './openapi31-webhooks.config.json'; 5 | import webhooksSpec from './openapi31-webhooks.json'; 6 | const spec = webhooksSpec as OpenAPIObject; 7 | 8 | const gen = new NgOpenApiGen(spec, options); 9 | gen.generate(); 10 | 11 | describe('OpenAPI 3.1 Webhooks Tests', () => { 12 | it('should generate User model with discriminator', () => { 13 | const model = gen.models.get('User'); 14 | expect(model).toBeDefined(); 15 | if (model) { 16 | const ts = gen.templates.apply('model', model); 17 | const parser = new TypescriptParser(); 18 | parser.parseSource(ts).then(ast => { 19 | expect(ast.declarations.length).toBe(1); 20 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 21 | const decl = ast.declarations[0] as InterfaceDeclaration; 22 | expect(decl.name).toBe('User'); 23 | 24 | // Check id property - should be string (uuid format) 25 | const idProp = decl.properties.find(p => p.name === 'id'); 26 | expect(idProp).toBeDefined(); 27 | expect(idProp?.type).toContain('string'); 28 | 29 | // Check name property - should be string 30 | const nameProp = decl.properties.find(p => p.name === 'name'); 31 | expect(nameProp).toBeDefined(); 32 | expect(nameProp?.type).toContain('string'); 33 | 34 | // Check userType property - should be enum 35 | const userTypeProp = decl.properties.find(p => p.name === 'userType'); 36 | expect(userTypeProp).toBeDefined(); 37 | expect(userTypeProp?.type).toContain('regular'); 38 | expect(userTypeProp?.type).toContain('admin'); 39 | }); 40 | } 41 | }); 42 | 43 | it('should generate WebhookPayload model with const and enum', () => { 44 | const model = gen.models.get('WebhookPayload'); 45 | expect(model).toBeDefined(); 46 | if (model) { 47 | const ts = gen.templates.apply('model', model); 48 | const parser = new TypescriptParser(); 49 | parser.parseSource(ts).then(ast => { 50 | expect(ast.declarations.length).toBe(1); 51 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 52 | const decl = ast.declarations[0] as InterfaceDeclaration; 53 | expect(decl.name).toBe('WebhookPayload'); 54 | 55 | // Check event property - should be enum 56 | const eventProp = decl.properties.find(p => p.name === 'event'); 57 | expect(eventProp).toBeDefined(); 58 | expect(eventProp?.type).toContain('user.created'); 59 | expect(eventProp?.type).toContain('user.updated'); 60 | expect(eventProp?.type).toContain('user.deleted'); 61 | 62 | // Check version property - should be const literal 63 | const versionProp = decl.properties.find(p => p.name === 'version'); 64 | expect(versionProp).toBeDefined(); 65 | expect(versionProp?.type).toContain('1.0'); 66 | 67 | // Check data property - should reference User 68 | const dataProp = decl.properties.find(p => p.name === 'data'); 69 | expect(dataProp).toBeDefined(); 70 | expect(dataProp?.type).toContain('User'); 71 | }); 72 | } 73 | }); 74 | 75 | it('should generate API service with webhook operations', () => { 76 | const service = gen.services.get('Api'); 77 | expect(service).toBeDefined(); 78 | if (service) { 79 | const ts = gen.templates.apply('service', service); 80 | const parser = new TypescriptParser(); 81 | parser.parseSource(ts).then(ast => { 82 | expect(ast.declarations.length).toBe(1); 83 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 84 | const cls = ast.declarations[0] as ClassDeclaration; 85 | 86 | // Should have createUser method 87 | const createUserMethod = cls.methods.find(m => m.name.includes('createUser')); 88 | expect(createUserMethod).toBeDefined(); 89 | 90 | // Should have getWebhookPayload method 91 | const getWebhookPayloadMethod = cls.methods.find(m => m.name.includes('getWebhookPayload')); 92 | expect(getWebhookPayloadMethod).toBeDefined(); 93 | }); 94 | } 95 | }); 96 | 97 | it('should handle webhooks specification without generating webhook services', () => { 98 | // Test that the generator doesn't fail when webhooks are present 99 | // Webhooks in OpenAPI 3.1 define callback operations but don't generate client services 100 | expect(gen.models.size).toBeGreaterThan(0); 101 | expect(gen.services.size).toBeGreaterThan(0); 102 | 103 | // Verify the spec contains webhooks 104 | expect((spec as any).webhooks).toBeDefined(); 105 | expect((spec as any).webhooks.userCreated).toBeDefined(); 106 | expect((spec as any).webhooks.userUpdated).toBeDefined(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/openapi31-nullable.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, InterfaceDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import options from './openapi31-nullable.config.json'; 5 | import nullableSpec from './openapi31-nullable.json'; 6 | const spec = nullableSpec as OpenAPIObject; 7 | 8 | const gen = new NgOpenApiGen(spec, options); 9 | gen.generate(); 10 | 11 | describe('OpenAPI 3.1 Nullable Types Tests', () => { 12 | it('should generate NullableArrayResponse model with union types', () => { 13 | const model = gen.models.get('NullableArrayResponse'); 14 | expect(model).toBeDefined(); 15 | if (model) { 16 | const ts = gen.templates.apply('model', model); 17 | const parser = new TypescriptParser(); 18 | parser.parseSource(ts).then(ast => { 19 | expect(ast.declarations.length).toBe(1); 20 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 21 | const decl = ast.declarations[0] as InterfaceDeclaration; 22 | expect(decl.name).toBe('NullableArrayResponse'); 23 | 24 | // Check items property - should handle array | null union 25 | const itemsProp = decl.properties.find(p => p.name === 'items'); 26 | expect(itemsProp).toBeDefined(); 27 | expect(itemsProp?.type).toContain('Array | null'); 28 | 29 | // Check nullableString property - should handle string | null 30 | const nullableStringProp = decl.properties.find(p => p.name === 'nullableString'); 31 | expect(nullableStringProp).toBeDefined(); 32 | expect(nullableStringProp?.type).toContain('string'); 33 | }); 34 | } 35 | }); 36 | 37 | it('should generate MixedTypesInput model with union and const types', () => { 38 | const model = gen.models.get('MixedTypesInput'); 39 | expect(model).toBeDefined(); 40 | if (model) { 41 | const ts = gen.templates.apply('model', model); 42 | const parser = new TypescriptParser(); 43 | parser.parseSource(ts).then(ast => { 44 | const decl = ast.declarations[0] as InterfaceDeclaration; 45 | expect(decl.name).toBe('MixedTypesInput'); 46 | 47 | // Check unionType property - should be string | number | boolean 48 | const unionTypeProp = decl.properties.find(p => p.name === 'unionType'); 49 | expect(unionTypeProp).toBeDefined(); 50 | expect(unionTypeProp?.type).toContain('string | number | boolean'); // Check constValue property - should be literal type 51 | const constValueProp = decl.properties.find(p => p.name === 'constValue'); 52 | expect(constValueProp).toBeDefined(); 53 | expect(constValueProp?.type).toContain('fixed_value'); 54 | 55 | // Check enumWithNull property 56 | const enumWithNullProp = decl.properties.find(p => p.name === 'enumWithNull'); 57 | expect(enumWithNullProp).toBeDefined(); 58 | expect(enumWithNullProp?.type).toContain('option1'); 59 | expect(enumWithNullProp?.type).toContain('option2'); 60 | }); 61 | } 62 | }); 63 | 64 | it('should generate JsonSchemaTypes model with complex union types', () => { 65 | const model = gen.models.get('JsonSchemaTypes'); 66 | expect(model).toBeDefined(); 67 | if (model) { 68 | const ts = gen.templates.apply('model', model); 69 | const parser = new TypescriptParser(); 70 | parser.parseSource(ts).then(ast => { 71 | const decl = ast.declarations[0] as InterfaceDeclaration; 72 | expect(decl.name).toBe('JsonSchemaTypes'); 73 | 74 | // Check unionField property 75 | const unionFieldProp = decl.properties.find(p => p.name === 'unionField'); 76 | expect(unionFieldProp).toBeDefined(); 77 | expect(unionFieldProp?.type).toContain('string'); 78 | 79 | // Check constField property 80 | const constFieldProp = decl.properties.find(p => p.name === 'constField'); 81 | expect(constFieldProp).toBeDefined(); 82 | expect(constFieldProp?.type).toContain('constant_value'); 83 | }); 84 | } 85 | }); 86 | 87 | it('should generate API service with nullable operations', () => { 88 | const service = gen.services.get('Api'); 89 | expect(service).toBeDefined(); 90 | if (service) { 91 | const ts = gen.templates.apply('service', service); 92 | const parser = new TypescriptParser(); 93 | parser.parseSource(ts).then(ast => { 94 | expect(ast.declarations.length).toBe(1); 95 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 96 | const cls = ast.declarations[0] as ClassDeclaration; 97 | 98 | // Should have getNullableArray method 99 | const getNullableArrayMethod = cls.methods.find(m => m.name.includes('getNullableArray')); 100 | expect(getNullableArrayMethod).toBeDefined(); 101 | 102 | // Should have postMixedTypes method 103 | const postMixedTypesMethod = cls.methods.find(m => m.name.includes('postMixedTypes')); 104 | expect(postMixedTypesMethod).toBeDefined(); 105 | }); 106 | } 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/openapi31-webhooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "OpenAPI 3.1 Webhooks Test", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/users": { 9 | "post": { 10 | "operationId": "createUser", 11 | "requestBody": { 12 | "content": { 13 | "application/json": { 14 | "schema": { 15 | "$ref": "#/components/schemas/User" 16 | } 17 | } 18 | } 19 | }, 20 | "responses": { 21 | "201": { 22 | "description": "User created successfully", 23 | "content": { 24 | "application/json": { 25 | "schema": { 26 | "$ref": "#/components/schemas/User" 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | }, 34 | "/webhook-payload": { 35 | "get": { 36 | "operationId": "getWebhookPayload", 37 | "responses": { 38 | "200": { 39 | "description": "Example webhook payload structure", 40 | "content": { 41 | "application/json": { 42 | "schema": { 43 | "$ref": "#/components/schemas/WebhookPayload" 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "webhooks": { 53 | "userCreated": { 54 | "post": { 55 | "operationId": "userCreatedWebhook", 56 | "summary": "User created webhook", 57 | "description": "Sent when a user is created", 58 | "requestBody": { 59 | "content": { 60 | "application/json": { 61 | "schema": { 62 | "$ref": "#/components/schemas/WebhookPayload" 63 | } 64 | } 65 | } 66 | }, 67 | "responses": { 68 | "200": { 69 | "description": "Webhook received successfully" 70 | } 71 | } 72 | } 73 | }, 74 | "userUpdated": { 75 | "post": { 76 | "operationId": "userUpdatedWebhook", 77 | "summary": "User updated webhook", 78 | "description": "Sent when a user is updated", 79 | "requestBody": { 80 | "content": { 81 | "application/json": { 82 | "schema": { 83 | "$ref": "#/components/schemas/WebhookPayload" 84 | } 85 | } 86 | } 87 | }, 88 | "responses": { 89 | "200": { 90 | "description": "Webhook received successfully" 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | "components": { 97 | "schemas": { 98 | "User": { 99 | "type": "object", 100 | "discriminator": { 101 | "propertyName": "userType", 102 | "mapping": { 103 | "regular": "#/components/schemas/RegularUser", 104 | "admin": "#/components/schemas/AdminUser" 105 | } 106 | }, 107 | "required": ["id", "name", "userType"], 108 | "properties": { 109 | "id": { 110 | "type": "string", 111 | "format": "uuid" 112 | }, 113 | "name": { 114 | "type": "string" 115 | }, 116 | "userType": { 117 | "type": "string", 118 | "enum": ["regular", "admin"] 119 | } 120 | } 121 | }, 122 | "RegularUser": { 123 | "allOf": [ 124 | { 125 | "$ref": "#/components/schemas/User" 126 | }, 127 | { 128 | "type": "object", 129 | "properties": { 130 | "userType": { 131 | "const": "regular" 132 | }, 133 | "subscription": { 134 | "type": "string", 135 | "enum": ["free", "premium"] 136 | } 137 | } 138 | } 139 | ] 140 | }, 141 | "AdminUser": { 142 | "allOf": [ 143 | { 144 | "$ref": "#/components/schemas/User" 145 | }, 146 | { 147 | "type": "object", 148 | "properties": { 149 | "userType": { 150 | "const": "admin" 151 | }, 152 | "permissions": { 153 | "type": "array", 154 | "items": { 155 | "type": "string", 156 | "enum": ["read", "write", "delete", "manage"] 157 | } 158 | } 159 | } 160 | } 161 | ] 162 | }, 163 | "WebhookPayload": { 164 | "type": "object", 165 | "required": ["event", "data"], 166 | "properties": { 167 | "event": { 168 | "type": "string", 169 | "enum": ["user.created", "user.updated", "user.deleted"] 170 | }, 171 | "data": { 172 | "$ref": "#/components/schemas/User" 173 | }, 174 | "version": { 175 | "const": "1.0" 176 | }, 177 | "timestamp": { 178 | "type": "string", 179 | "format": "date-time" 180 | } 181 | } 182 | } 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /test/openapi31-jsonschema.spec.ts: -------------------------------------------------------------------------------- 1 | import { ClassDeclaration, InterfaceDeclaration, TypescriptParser } from 'typescript-parser'; 2 | import { NgOpenApiGen } from '../lib/ng-openapi-gen'; 3 | import { OpenAPIObject } from '../lib/openapi-typings'; 4 | import options from './openapi31-jsonschema.config.json'; 5 | import jsonschemaSpec from './openapi31-jsonschema.json'; 6 | const spec = jsonschemaSpec as OpenAPIObject; 7 | 8 | const gen = new NgOpenApiGen(spec, options); 9 | gen.generate(); 10 | 11 | describe('OpenAPI 3.1 JSON Schema Features Tests', () => { 12 | it('should handle prefixItems as tuple types', () => { 13 | const model = gen.models.get('AdvancedSchemaFeatures'); 14 | expect(model).toBeDefined(); 15 | if (model) { 16 | const ts = gen.templates.apply('model', model); 17 | const parser = new TypescriptParser(); 18 | parser.parseSource(ts).then(ast => { 19 | expect(ast.declarations.length).toBe(1); 20 | expect(ast.declarations[0]).toEqual(expect.any(InterfaceDeclaration)); 21 | const decl = ast.declarations[0] as InterfaceDeclaration; 22 | 23 | // Check tupleArray property - should be tuple type [string, number, boolean] 24 | const tupleArrayProp = decl.properties.find(p => p.name === 'tupleArray'); 25 | expect(tupleArrayProp).toBeDefined(); 26 | // OpenAPI 3.1 prefixItems now correctly generates tuple types 27 | expect(tupleArrayProp?.type).toContain('[string, number, boolean]'); 28 | }); 29 | } 30 | }); 31 | 32 | it('should handle discriminator with mapping', () => { 33 | const model = gen.models.get('Dog'); 34 | expect(model).toBeDefined(); 35 | if (model) { 36 | const ts = gen.templates.apply('model', model); 37 | const parser = new TypescriptParser(); 38 | parser.parseSource(ts).then(ast => { 39 | const decl = ast.declarations[0] as InterfaceDeclaration; 40 | expect(decl.name).toBe('Dog'); 41 | 42 | // Check pet_type property - should be const "dog" 43 | const petTypeProp = decl.properties.find(p => p.name === 'pet_type'); 44 | expect(petTypeProp).toBeDefined(); 45 | expect(petTypeProp?.type).toContain('dog'); 46 | 47 | // Check breed property - should be enum 48 | const breedProp = decl.properties.find(p => p.name === 'breed'); 49 | expect(breedProp).toBeDefined(); 50 | expect(breedProp?.type).toContain('Dingo'); 51 | expect(breedProp?.type).toContain('Husky'); 52 | expect(breedProp?.type).toContain('Retriever'); 53 | }); 54 | } 55 | }); 56 | 57 | it('should handle Cat model properly', () => { 58 | const model = gen.models.get('Cat'); 59 | expect(model).toBeDefined(); 60 | if (model) { 61 | const ts = gen.templates.apply('model', model); 62 | const parser = new TypescriptParser(); 63 | parser.parseSource(ts).then(ast => { 64 | const decl = ast.declarations[0] as InterfaceDeclaration; 65 | expect(decl.name).toBe('Cat'); 66 | 67 | // Check pet_type property - should be const "cat" 68 | const petTypeProp = decl.properties.find(p => p.name === 'pet_type'); 69 | expect(petTypeProp).toBeDefined(); 70 | expect(petTypeProp?.type).toContain('cat'); 71 | }); 72 | } 73 | }); 74 | 75 | it('should handle ResponseWithDiscriminator as union type', () => { 76 | const model = gen.models.get('ResponseWithDiscriminator'); 77 | expect(model).toBeDefined(); 78 | if (model) { 79 | const ts = gen.templates.apply('model', model); 80 | const parser = new TypescriptParser(); 81 | parser.parseSource(ts).then(ast => { 82 | const decl = ast.declarations[0] as InterfaceDeclaration; 83 | expect(decl.name).toBe('ResponseWithDiscriminator'); 84 | 85 | // Should reference Dog and Cat types 86 | expect(ts).toContain('Dog'); 87 | expect(ts).toContain('Cat'); 88 | }); 89 | } 90 | }); 91 | 92 | it('should handle ArrayWithContains model', () => { 93 | const model = gen.models.get('ArrayWithContains'); 94 | expect(model).toBeDefined(); 95 | if (model) { 96 | const ts = gen.templates.apply('model', model); 97 | const parser = new TypescriptParser(); 98 | parser.parseSource(ts).then(ast => { 99 | const decl = ast.declarations[0] as InterfaceDeclaration; 100 | expect(decl.name).toBe('ArrayWithContains'); 101 | 102 | // Check mixedArray property - should handle contains constraint 103 | const mixedArrayProp = decl.properties.find(p => p.name === 'mixedArray'); 104 | expect(mixedArrayProp).toBeDefined(); 105 | // TODO: contains is advanced feature, may fall back to basic array 106 | expect(mixedArrayProp?.type).toContain('Array'); 107 | 108 | 109 | }); 110 | } 111 | }); 112 | 113 | it('should generate service with testSchemaFeatures method', () => { 114 | const service = gen.services.get('Api'); 115 | expect(service).toBeDefined(); 116 | if (service) { 117 | const ts = gen.templates.apply('service', service); 118 | const parser = new TypescriptParser(); 119 | parser.parseSource(ts).then(ast => { 120 | expect(ast.declarations.length).toBe(1); 121 | expect(ast.declarations[0]).toEqual(expect.any(ClassDeclaration)); 122 | const cls = ast.declarations[0] as ClassDeclaration; 123 | 124 | // Should have testSchemaFeatures method 125 | const testSchemaFeaturesMethod = cls.methods.find(m => m.name.includes('testSchemaFeatures')); 126 | expect(testSchemaFeaturesMethod).toBeDefined(); 127 | }); 128 | } 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /lib/operation-variant.ts: -------------------------------------------------------------------------------- 1 | import { upperFirst } from 'lodash'; 2 | import { Content } from './content'; 3 | import { GenType } from './gen-type'; 4 | import { ensureNotReserved, fileName, resolveRef, tsComments } from './gen-utils'; 5 | import { Importable } from './importable'; 6 | import { SchemaObject, isReferenceObject } from './openapi-typings'; 7 | import { Operation } from './operation'; 8 | import { Options } from './options'; 9 | 10 | /** 11 | * An operation has a variant per distinct possible body content 12 | */ 13 | export class OperationVariant extends GenType implements Importable { 14 | responseMethodName: string; 15 | resultType: string; 16 | responseType: string; 17 | accept: string; 18 | isVoid: boolean; 19 | isNumber: boolean; 20 | isBoolean: boolean; 21 | isOther: boolean; 22 | responseMethodTsComments: string; 23 | bodyMethodTsComments: string; 24 | 25 | paramsType: string; 26 | paramsImport: Importable; 27 | 28 | importName: string; 29 | importPath: string; 30 | importFile: string; 31 | exportName: string; 32 | paramsTypeExportName: string; 33 | 34 | constructor( 35 | public operation: Operation, 36 | public methodName: string, 37 | public requestBody: Content | null, 38 | public successResponse: Content | null, 39 | public options: Options) { 40 | 41 | super(methodName, n => n, options); 42 | 43 | this.responseMethodName = `${methodName}$Response`; 44 | if (successResponse) { 45 | this.resultType = successResponse.type; 46 | this.responseType = this.inferResponseType(successResponse, operation, options); 47 | this.accept = successResponse.mediaType; 48 | } else { 49 | this.resultType = 'void'; 50 | this.responseType = 'text'; 51 | this.accept = '*/*'; 52 | } 53 | this.isVoid = this.resultType === 'void'; 54 | this.isNumber = this.resultType === 'number'; 55 | this.isBoolean = this.resultType === 'boolean'; 56 | this.isOther = !this.isVoid && !this.isNumber && !this.isBoolean; 57 | this.responseMethodTsComments = tsComments(this.responseMethodDescription(), 1, operation.deprecated); 58 | this.bodyMethodTsComments = tsComments(this.bodyMethodDescription(), 1, operation.deprecated); 59 | 60 | this.importPath = 'fn/' + fileName(this.operation.tag); 61 | this.importName = ensureNotReserved(methodName); 62 | this.importFile = fileName(methodName); 63 | 64 | this.paramsType = `${upperFirst(methodName)}$Params`; 65 | this.paramsImport = { 66 | importName: this.paramsType, 67 | importFile: this.importFile, 68 | importPath: this.importPath 69 | }; 70 | 71 | // Collect parameter imports 72 | for (const parameter of this.operation.parameters) { 73 | this.collectImports(parameter.spec.schema, false, true); 74 | } 75 | // Collect the request body imports 76 | this.collectImports(this.requestBody?.spec?.schema); 77 | // Collect the response imports 78 | this.collectImports(this.successResponse?.spec?.schema); 79 | 80 | // Finally, update the imports 81 | this.updateImports(); 82 | } 83 | 84 | private inferResponseType(successResponse: Content, operation: Operation, { customizedResponseType = {} }: Pick): string { 85 | const customizedResponseTypeByPath = customizedResponseType[operation.path]; 86 | if (customizedResponseTypeByPath) { 87 | return customizedResponseTypeByPath.toUse; 88 | } 89 | 90 | // When the schema is in binary format, return 'blob' 91 | let schemaOrRef = successResponse.spec?.schema || { type: 'string' }; 92 | if (isReferenceObject(schemaOrRef)) { 93 | schemaOrRef = resolveRef(operation.openApi, schemaOrRef.$ref); 94 | } 95 | const schema = schemaOrRef as SchemaObject; 96 | if (schema.format === 'binary') { 97 | return 'blob'; 98 | } 99 | 100 | const mediaType = successResponse.mediaType.toLowerCase(); 101 | if (mediaType.includes('/json') || mediaType.includes('+json')) { 102 | return 'json'; 103 | } else if (mediaType.startsWith('text/')) { 104 | return 'text'; 105 | } else { 106 | return 'blob'; 107 | } 108 | } 109 | 110 | private responseMethodDescription() { 111 | return `${this.descriptionPrefix()}This method provides access to the full \`HttpResponse\`, allowing access to response headers. 112 | To access only the response body, use \`${this.methodName}()\` instead.${this.descriptionSuffix()}`; 113 | } 114 | 115 | private bodyMethodDescription() { 116 | return `${this.descriptionPrefix()}This method provides access only to the response body. 117 | To access the full response (for headers, for example), \`${this.responseMethodName}()\` instead.${this.descriptionSuffix()}`; 118 | } 119 | 120 | private descriptionPrefix() { 121 | let description = (this.operation.spec.description || '').trim(); 122 | let summary = this.operation.spec.summary; 123 | if (summary) { 124 | if (!summary.endsWith('.')) { 125 | summary += '.'; 126 | } 127 | description = summary + '\n\n' + description; 128 | } 129 | if (description !== '') { 130 | description += '\n\n'; 131 | } 132 | return description; 133 | } 134 | 135 | private descriptionSuffix() { 136 | const sends = this.requestBody ? 'sends `' + this.requestBody.mediaType + '` and ' : ''; 137 | const handles = this.requestBody 138 | ? `handles request body of type \`${this.requestBody.mediaType}\`` 139 | : 'doesn\'t expect any request body'; 140 | return `\n\nThis method ${sends}${handles}.`; 141 | } 142 | 143 | protected skipImport(): boolean { 144 | return false; 145 | } 146 | 147 | protected initPathToRoot(): string { 148 | return this.importPath.split(/\//g).map(() => '..').join('/') + '/'; 149 | } 150 | 151 | get tag() { 152 | return this.operation.tags[0] || this.options.defaultTag || 'Api'; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/options.ts: -------------------------------------------------------------------------------- 1 | 2 | /** Options used by ng-openapi-gen */ 3 | export interface Options { 4 | 5 | /** The input file or URL to the OpenAPI 3 specification, JSON or YAML, local file or URL */ 6 | input: string; 7 | 8 | /** Where generated files will be written to. Defaults to 'src/app/api'. */ 9 | output?: string; 10 | 11 | /** Tag name assumed for operations without tags. Defaults to the value of 'prefix', which defaults to 'Api'. */ 12 | defaultTag?: string; 13 | 14 | /** Indicates the timeout when fetching the input URL, in milliseconds. Defaults to 20 seconds (20000). */ 15 | fetchTimeout?: number; 16 | 17 | /** Specific tags to be included */ 18 | includeTags?: string[]; 19 | 20 | /** Specific tags to be excluded */ 21 | excludeTags?: string[]; 22 | 23 | /** Whether to skip models without references to them */ 24 | ignoreUnusedModels?: boolean; 25 | 26 | /** Whether to remove unexpected files in the output directory */ 27 | removeStaleFiles?: boolean; 28 | 29 | /** Typescript file, without '.ts' extension that exports all models. Set to false to skip. Defaults to `models`. */ 30 | modelIndex?: string | boolean; 31 | 32 | /** Typescript file, without '.ts' extension that exports all functions. Set to false to skip. Defaults to `functions`. */ 33 | functionIndex?: string | boolean; 34 | 35 | /** Typescript file, without '.ts' extension that exports all services. Set to false to skip. Defaults to `services`. */ 36 | serviceIndex?: string | boolean; 37 | 38 | /** When true, an 'index.ts' file will be generated, exporting all generated files */ 39 | indexFile?: boolean; 40 | 41 | /** When false, no services will be generated (clients will use functions directly) */ 42 | services?: boolean; 43 | 44 | /** When true, generates Promise-based service methods. When false, Observable-based. */ 45 | promises?: boolean; 46 | 47 | /** Prefix for generated service classes. Defaults to empty. */ 48 | servicePrefix?: string; 49 | 50 | /** Suffix for generated service classes. Defaults to 'Service'. */ 51 | serviceSuffix?: string; 52 | 53 | /** Prefix for generated model classes. Defaults to empty. */ 54 | modelPrefix?: string; 55 | 56 | /** Suffix for generated model classes. Defaults to empty. */ 57 | modelSuffix?: string; 58 | 59 | /** Whether to generate the module which provides all services */ 60 | apiModule?: boolean; 61 | 62 | /** Name for the configuration class to generate. Defaults to 'ApiConfiguration'. */ 63 | configuration?: string; 64 | 65 | /** Name for the base service class to generate. Defaults to 'BaseService'. */ 66 | baseService?: string; 67 | 68 | /** Name for the service to call functions directly. Defaults to 'Api'. */ 69 | apiService?: string | boolean; 70 | 71 | /** Name for the request builder class to generate. Defaults to 'RequestBuilder'. */ 72 | requestBuilder?: string; 73 | 74 | /** Name for the response class to generate. Defaults to 'StrictHttpResponse'. */ 75 | response?: string; 76 | 77 | /** Class name of the module that provides all services. Set to false to skip. Defaults to `ApiModule`. */ 78 | module?: string | boolean; 79 | 80 | /** 81 | * Determines how root enums will be generated. Possible values are: 82 | * 83 | * - `alias`: just generate a type alias with the possible values; 84 | * - `upper` for an enum with UPPER_CASE names; 85 | * - `pascal` for enum PascalCase names; 86 | * - `ignorecase` for enum names that ignore character casing; 87 | * 88 | * Defaults to 'alias'. 89 | */ 90 | enumStyle?: 'alias' | 'upper' | 'pascal' | 'ignorecase'; 91 | 92 | /** 93 | * Should an array with all enum items of models be exported in a sibling file for enums? 94 | */ 95 | enumArray?: boolean; 96 | 97 | /** 98 | * Determines how to normalize line endings. Possible values are: 99 | * 100 | * - `crlf`: normalize line endings to CRLF (Windows, DOS) => \r\n 101 | * - `lf` normalize line endings to LF (Unix, OS X) => \n 102 | * - `cr` normalize line endings to CR (Mac OS) => \r 103 | * - `auto` normalize line endings for the current operating system 104 | * 105 | * Defaults to 'auto'. 106 | */ 107 | endOfLineStyle?: 'crlf' | 'lf' | 'cr' | 'auto'; 108 | 109 | /** Custom templates directory. Any `.handlebars` files here will be used instead of the corresponding default. */ 110 | templates?: string; 111 | 112 | /** When specified, filters the generated services, excluding any param corresponding to this list of params. */ 113 | excludeParameters?: string[]; 114 | 115 | /** When specified, does not generate a $Json suffix. */ 116 | skipJsonSuffix?: boolean; 117 | 118 | customizedResponseType?: { 119 | [key: string]: { 120 | toUse: 'arraybuffer' | 'blob' | 'json' | 'document'; 121 | }; 122 | }; 123 | 124 | /** When specified, will create temporary files in system temporary folder instead of next to output folder. */ 125 | useTempDir?: boolean; 126 | 127 | /** When true, no verbose output will be displayed */ 128 | silent?: boolean; 129 | 130 | /** When true (default) models names will be camelized, besides having the first letter capitalized. Setting to false will prevent camelizing. */ 131 | camelizeModelNames?: boolean; 132 | 133 | /** List of paths to early exclude from the processing */ 134 | excludePaths?: string[]; 135 | 136 | /** 137 | * When true, the expected response type in the request method names are not abbreviated and all response variants are kept. 138 | * Default is false. 139 | * When array is given, `mediaType` is expected to be a RegExp string matching the response media type. The first match in the array 140 | * will decide whether or how to shorten the media type. If no mediaType is given, it will always match. 141 | * 142 | * 'short': application/x-spring-data-compact+json -> getEntities$Json 143 | * 'tail': application/x-spring-data-compact+json -> getEntities$XSpringDataCompactJson 144 | * 'full': application/x-spring-data-compact+json -> getEntities$ApplicationXSpringDataCompactJson 145 | */ 146 | keepFullResponseMediaType?: boolean | Array<{ mediaType?: string; use: 'full' | 'tail' | 'short' }>; 147 | } 148 | --------------------------------------------------------------------------------