Repository: lukeautry/tsoa
Pull request #656 (2020-04-09 15:50:16)

feat: handle enum members
### All Submissions:

* [x] Have you followed the guidelines in our [Contributing](https://github.com/lukeautry/tsoa/tree/master/docs/CONTRIBUTING.md) document?
* [x] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/lukeautry/tsoa/pulls) for the same update/change?
* [x] Have you written unit tests?
* [ ] Have you written unit tests that cover the negative cases (i.e.: if bad data is submitted, does the library respond properly)?
* [x] This PR is associated with an existing issue?

**Closing issues**

Closes 655

### If this is a new feature submission:

* [ ] Has the issue had a maintainer respond to the issue and clarify that the feature is something that aligns with the [goals](https://github.com/lukeautry/tsoa#goal) and [philosophy](https://github.com/lukeautry/tsoa#philosophy) of the project?

**Potential Problems With The Approach**

I'm not really convinced about how I've implemented it, but I've haven't found another way without duplicating or breaking code


Patch:
diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts
index 4a40d7e0..ddfe4a7d 100644
--- a/src/metadataGeneration/typeResolver.ts
+++ b/src/metadataGeneration/typeResolver.ts
@@ -173,7 +173,7 @@ export class TypeResolver {
       if (type.aliasSymbol) {
         let declaration = type.aliasSymbol.declarations[0] as ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.DeclarationStatement;
         if (declaration.name) {
-          declaration = this.getModelTypeDeclaration(declaration.name as ts.EntityName);
+          declaration = this.getModelTypeDeclaration(declaration.name as ts.EntityName) as ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.DeclarationStatement;
         }
         const name = this.getRefTypeName(this.referencer.getText());
         return this.handleCachingAndCircularReferences(name, () => {
@@ -425,6 +425,12 @@ export class TypeResolver {
       let referenceType: Tsoa.ReferenceType;
       if (ts.isTypeAliasDeclaration(declaration)) {
         referenceType = this.getTypeAliasReference(declaration, name, node);
+      } else if (ts.isEnumMember(declaration)) {
+        referenceType = {
+          dataType: 'refEnum',
+          refName: this.getRefTypeName(name),
+          enums: [this.current.typeChecker.getConstantValue(declaration)!],
+        };
       } else {
         referenceType = this.getModelReference(declaration, name);
       }
@@ -580,6 +586,7 @@ export class TypeResolver {
       case ts.SyntaxKind.ClassDeclaration:
       case ts.SyntaxKind.TypeAliasDeclaration:
       case ts.SyntaxKind.EnumDeclaration:
+      case ts.SyntaxKind.EnumMember:
         return true;
       default:
         return false;
@@ -597,13 +604,13 @@ export class TypeResolver {
     while (leftmost.parent && leftmost.parent.kind === ts.SyntaxKind.QualifiedName) {
       const leftmostName = leftmost.kind === ts.SyntaxKind.Identifier ? (leftmost as ts.Identifier).text : (leftmost as ts.QualifiedName).right.text;
       const moduleDeclarations = statements.filter(node => {
-        if (node.kind !== ts.SyntaxKind.ModuleDeclaration || !this.current.IsExportedNode(node)) {
+        if ((node.kind !== ts.SyntaxKind.ModuleDeclaration || !this.current.IsExportedNode(node)) && !ts.isEnumDeclaration(node)) {
           return false;
         }
 
-        const moduleDeclaration = node as ts.ModuleDeclaration;
+        const moduleDeclaration = node as ts.ModuleDeclaration | ts.EnumDeclaration;
         return (moduleDeclaration.name as ts.Identifier).text.toLowerCase() === leftmostName.toLowerCase();
-      }) as ts.ModuleDeclaration[];
+      }) as Array<ts.ModuleDeclaration | ts.EnumDeclaration>;
 
       if (!moduleDeclarations.length) {
         throw new GenerateMetadataError(`No matching module declarations found for ${leftmostName}.`);
@@ -612,12 +619,15 @@ export class TypeResolver {
         throw new GenerateMetadataError(`Multiple matching module declarations found for ${leftmostName}; please make module declarations unique.`);
       }
 
-      const moduleBlock = moduleDeclarations[0].body as ts.ModuleBlock;
-      if (moduleBlock === null || moduleBlock.kind !== ts.SyntaxKind.ModuleBlock) {
-        throw new GenerateMetadataError(`Module declaration found for ${leftmostName} has no body.`);
+      if (ts.isEnumDeclaration(moduleDeclarations[0])) {
+        statements = moduleDeclarations[0].members;
+      } else {
+        const moduleBlock = moduleDeclarations[0].body as ts.ModuleBlock;
+        if (moduleBlock === null || moduleBlock.kind !== ts.SyntaxKind.ModuleBlock) {
+          throw new GenerateMetadataError(`Module declaration found for ${leftmostName} has no body.`);
+        }
+        statements = moduleBlock.statements;
       }
-
-      statements = moduleBlock.statements;
       leftmost = leftmost.parent as ts.EntityName;
     }
 
@@ -635,9 +645,9 @@ export class TypeResolver {
         return false;
       }
 
-      const modelTypeDeclaration = node as UsableDeclaration;
+      const modelTypeDeclaration = node as UsableDeclaration | ts.EnumMember;
       return (modelTypeDeclaration.name as ts.Identifier).text === typeName;
-    }) as Array<Exclude<UsableDeclaration, ts.PropertySignature>>;
+    }) as Array<Exclude<UsableDeclaration | ts.EnumMember, ts.PropertySignature>>;
 
     if (!modelTypes.length) {
       throw new GenerateMetadataError(
@@ -799,7 +809,8 @@ export class TypeResolver {
   private typeArgumentsToContext(type: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, targetEntitiy: ts.EntityName, context: Context): Context {
     this.context = {};
 
-    const typeParameters = this.getModelTypeDeclaration(targetEntitiy).typeParameters;
+    const declaration = this.getModelTypeDeclaration(targetEntitiy);
+    const typeParameters = 'typeParameters' in declaration ? declaration.typeParameters : undefined;
 
     if (typeParameters) {
       for (let index = 0; index < typeParameters.length; index++) {


Test patch:
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index d60975bd..8cea61d7 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -48,6 +48,7 @@ export interface TestModel extends Model {
   enumStringNumberArray?: EnumStringNumberValue[];
   enumNumberArray?: EnumNumberValue[];
   enumStringValue?: EnumStringValue;
+  enumStringProperty?: EnumStringValue.VALUE_1;
   enumStringArray?: EnumStringValue[];
   modelValue: TestSubModel;
   modelsArray: TestSubModel[];
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index 2a9d7b3d..19bd0ecd 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -298,6 +298,15 @@ describe('Definition generation', () => {
             const expectedEnumValues = ['', 'VALUE_1', 'VALUE_2'];
             expect(validatedDefinition.enum).to.eql(expectedEnumValues, `for property ${propertyName}[enum]`);
           },
+          enumStringProperty: (propertyName, propertySchema) => {
+            expect(propertySchema.$ref).to.eq('#/definitions/EnumStringValue.VALUE_1');
+            const schema = getValidatedDefinition('EnumStringValue.VALUE_1', currentSpec);
+            expect(schema).to.deep.eq({
+              description: undefined,
+              enum: ['VALUE_1'],
+              type: 'string',
+            });
+          },
           enumStringArray: (propertyName, propertySchema) => {
             expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`);
             expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`);
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
index 9113e790..0f884faf 100644
--- a/tests/unit/swagger/schemaDetails3.spec.ts
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -487,6 +487,15 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
             expect(schema.type).to.eq('string');
             expect(schema.enum).to.eql(['', 'VALUE_1', 'VALUE_2']);
           },
+          enumStringProperty: (propertyName, propertySchema) => {
+            expect(propertySchema.$ref).to.eq('#/components/schemas/EnumStringValue.VALUE_1');
+            const schema = getComponentSchema('EnumStringValue.VALUE_1', currentSpec);
+            expect(schema).to.deep.eq({
+              description: undefined,
+              enum: ['VALUE_1'],
+              type: 'string',
+            });
+          },
           enumStringArray: (propertyName, propertySchema) => {
             expect(propertySchema.type).to.eq('array', `for property ${propertyName}.type`);
             expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`);
@@ -516,11 +525,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
 
             const componentSchema = getComponentSchema('StrLiteral', currentSpec);
             expect(componentSchema).to.deep.eq({
-              oneOf: [
-                { type: 'string', enum: [''], nullable: false },
-                { type: 'string', enum: ['Foo'], nullable: false },
-                { type: 'string', enum: ['Bar'], nullable: false },
-              ],
+              oneOf: [{ type: 'string', enum: [''], nullable: false }, { type: 'string', enum: ['Foo'], nullable: false }, { type: 'string', enum: ['Bar'], nullable: false }],
               default: undefined,
               description: undefined,
               example: undefined,
@@ -1001,10 +1006,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
                     description: undefined,
                     format: undefined,
                     example: undefined,
-                    oneOf: [
-                      { enum: ['OK'], nullable: false, type: 'string' },
-                      { enum: ['KO'], nullable: false, type: 'string' },
-                    ],
+                    oneOf: [{ enum: ['OK'], nullable: false, type: 'string' }, { enum: ['KO'], nullable: false, type: 'string' }],
                   },
                   id: { type: 'number', format: 'double', default: undefined, description: undefined, example: undefined },
                   keyInterface: { type: 'string', default: undefined, description: undefined, format: undefined, example: undefined, enum: ['id'], nullable: false },
@@ -1083,10 +1085,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
                 maybeString: { $ref: '#/components/schemas/Maybe_string_', description: undefined, format: undefined, example: undefined },
                 wordOrNull: { $ref: '#/components/schemas/Maybe_Word_', description: undefined, format: undefined, example: undefined },
                 numberOrNull: {
-                  oneOf: [
-                    { type: 'number', format: 'double' },
-                    { type: 'number', enum: ['null'], nullable: true },
-                  ],
+                  oneOf: [{ type: 'number', format: 'double' }, { type: 'number', enum: ['null'], nullable: true }],
                   description: undefined,
                   format: undefined,
                   default: undefined,
@@ -1177,10 +1176,7 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
       // Assert
       expect(getComponentSchema(schemaName, { specName: 'specDefault', spec })).to.deep.eq({
         description: undefined,
-        oneOf: [
-          { type: 'number', enum: [1, 3] },
-          { type: 'string', enum: ['two', 'four'] },
-        ],
+        oneOf: [{ type: 'number', enum: [1, 3] }, { type: 'string', enum: ['two', 'four'] }],
       });
     });
   });