Repository: lukeautry/tsoa
Pull request #359 (2019-06-07 16:59:30)

How to correctly generate open api 3 document 358
### API and Spec versions in separate configs

A few things to note:

- The `specVersion` is a number format, so only single (`Major`) spec versions supported.  This might not be ideal?
- Had to expose the `validateSwaggerConfig` method in the `cli.ts` file in order to add unit tests for the configs.


Patch:
diff --git a/.gitignore b/.gitignore
index f60b26ee..e51f71ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 dist
 node_modules
 routes.ts
+.idea/
diff --git a/src/cli.ts b/src/cli.ts
index 0d8e3642..05fef2ba 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -60,13 +60,17 @@ const validateCompilerOptions = (config?: ts.CompilerOptions): ts.CompilerOption
   return config || {};
 };
 
-const validateSwaggerConfig = async (config: SwaggerConfig): Promise<SwaggerConfig> => {
-  if (!config.outputDirectory) { throw new Error('Missing outputDirectory: onfiguration most contain output directory'); }
+export const validateSwaggerConfig = async (config: SwaggerConfig): Promise<SwaggerConfig> => {
+  if (!config.outputDirectory) { throw new Error('Missing outputDirectory: configuration must contain output directory.'); }
   if (!config.entryFile) { throw new Error('Missing entryFile: Configuration must contain an entry point file.'); }
   if (!await fsExists(config.entryFile)) {
     throw new Error(`EntryFile not found: ${config.entryFile} - Please check your tsoa config.`);
   }
   config.version = config.version || await versionDefault();
+
+  config.specVersion = config.specVersion || 2;
+  if (config.specVersion !== 2 && config.specVersion !== 3) { throw new Error('Unsupported Spec version.'); }
+
   config.name = config.name || await nameDefault();
   config.description = config.description || await descriptionDefault();
   config.license = config.license || await licenseDefault();
diff --git a/src/config.ts b/src/config.ts
index 7b314c5a..732a8a4d 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -47,6 +47,14 @@ export interface SwaggerConfig {
    */
   version?: string;
 
+  /**
+   * Major OpenAPI version to generate; defaults to version 2 when not specified
+   * Possible values:
+   *  - 2: generates OpenAPI version 2.
+   *  - 3: generates OpenAPI version 3.
+   */
+  specVersion?: Swagger.SupportedSpecMajorVersion;
+
   /**
    * API name; defaults to npm package name
    */
diff --git a/src/module/generate-swagger-spec.ts b/src/module/generate-swagger-spec.ts
index ea3fae74..a857104f 100644
--- a/src/module/generate-swagger-spec.ts
+++ b/src/module/generate-swagger-spec.ts
@@ -26,7 +26,7 @@ export const generateSwaggerSpec = async (
   }
 
   let spec: Swagger.Spec;
-  if (config.version && config.version.split('.')[0] === '3') {
+  if (config.specVersion && config.specVersion === 3) {
     spec = new SpecGenerator3(metadata, config).GetSpec();
   } else {
     spec = new SpecGenerator2(metadata, config).GetSpec();
diff --git a/src/swagger/swagger.ts b/src/swagger/swagger.ts
index 7dd25be7..aeb11445 100644
--- a/src/swagger/swagger.ts
+++ b/src/swagger/swagger.ts
@@ -21,6 +21,9 @@ export namespace Swagger {
     | 'ws'
     | 'wss';
 
+  export type SupportedSpecMajorVersion = 2
+    | 3;
+
   export interface Spec {
     info: Info;
     tags?: Tag[];
diff --git a/tsoa.json b/tsoa.json
index 08c0b70d..0c20ae39 100644
--- a/tsoa.json
+++ b/tsoa.json
@@ -20,7 +20,8 @@
                 }
             }
         },
-        "yaml": true
+        "yaml": true,
+        "specVersion": 2
     },
     "routes": {
         "basePath": "/v1",


Test patch:
diff --git a/tests/fixtures/defaultOptions.ts b/tests/fixtures/defaultOptions.ts
index 70ef9b15..2cb59729 100644
--- a/tests/fixtures/defaultOptions.ts
+++ b/tests/fixtures/defaultOptions.ts
@@ -1,13 +1,16 @@
 import { SwaggerConfig } from './../../src/config';
-export function getDefaultOptions(): SwaggerConfig {
+export function getDefaultOptions(
+  outputDirectory: string = '',
+  entryFile: string = ''): SwaggerConfig {
+
   return {
     basePath: '/v1',
     description: 'Description of a test API',
-    entryFile: '',
+    entryFile,
     host: 'localhost:3000',
     license: 'MIT',
     name: 'Test API',
-    outputDirectory: '',
+    outputDirectory,
     securityDefinitions: {
       basic: {
         type: 'basic',
diff --git a/tests/unit/swagger/config.spec.ts b/tests/unit/swagger/config.spec.ts
new file mode 100644
index 00000000..734375f2
--- /dev/null
+++ b/tests/unit/swagger/config.spec.ts
@@ -0,0 +1,78 @@
+import { expect } from 'chai';
+import 'mocha';
+import { validateSwaggerConfig } from '../../../src/cli';
+import { SwaggerConfig } from '../../../src/config';
+import { getDefaultOptions } from '../../fixtures/defaultOptions';
+
+describe('Configuration', () => {
+
+  it('should reject when outputDirectory is not set', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions();
+    validateSwaggerConfig(config).then((result) => {
+      throw new Error('Should not get here, expecting error regarding outputDirectory');
+    }, (err) => {
+      expect(err.message).to.equal('Missing outputDirectory: configuration must contain output directory.');
+      done();
+    });
+
+  });
+
+  it('should reject when entryFile is not set', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions('some/output/directory');
+    validateSwaggerConfig(config).then((result) => {
+      throw new Error('Should not get here, expecting error regarding entryFile');
+    }, (err) => {
+      expect(err.message).to.equal('Missing entryFile: Configuration must contain an entry point file.');
+      done();
+    });
+
+  });
+
+  it('should set the default API version', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions('some/output/directory', 'tsoa.json');
+    validateSwaggerConfig(config).then((configResult: SwaggerConfig) => {
+      expect(configResult.version).to.equal('1.0.0');
+      done();
+    });
+
+  });
+
+  it('should set the default Spec version 2 when not specified', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions('some/output/directory', 'tsoa.json');
+    validateSwaggerConfig(config).then((configResult: SwaggerConfig) => {
+      expect(configResult.specVersion).to.equal(2);
+      done();
+    });
+
+  });
+
+  it('should reject an unsupported Spec version', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions('some/output/directory', 'tsoa.json');
+    // Do any cast to ignore compile error due to Swagger.SupportedSpecVersion not supporting -2
+    config.specVersion = -2 as any;
+    validateSwaggerConfig(config).then((configResult: SwaggerConfig) => {
+      throw new Error('Should not get here, expecting error regarding unsupported Spec version');
+    }, (err) => {
+      expect(err.message).to.equal('Unsupported Spec version.');
+      done();
+    });
+
+  });
+
+  it('should accept Spec version 3 when specified', (done) => {
+
+    const config: SwaggerConfig = getDefaultOptions('some/output/directory', 'tsoa.json');
+    config.specVersion = 3;
+    validateSwaggerConfig(config).then((configResult: SwaggerConfig) => {
+      expect(configResult.specVersion).to.equal(3);
+      done();
+    });
+
+  });
+
+});