Repository: typestack/class-validator
Pull request #1826 (2022-12-03 13:47:07)

feat: return possible values in error message for `@IsEnum` decorator
## Description
<!-- A clear and concise description what these changes does. -->

This PR changes error message generation for `@IsEnum` decorator to include possible error options. 

## Checklist
<!-- Replace  the [ ] with [x] to check the boxes. -->
- [x] the pull request title describes what this PR does (not a vague title like `Update index.md`)
- [x] the pull request targets the *default* branch of the repository (`develop`)
- [x] the code follows the established code style of the repository
  - `npm run prettier:check` passes
  - `npm run lint:check` passes
- [x] tests are added for the changes I made (if any source code was modified)
- [ ] documentation added or updated
- [x] I have run the project locally and verified that there are no errors

### Fixes
<!-- If there is no issue being resolved, open one before creating this pull request. -->
<!-- If the PR doesn't fully resolve the issue, replace 'fixes' with 'references'. -->
references https://github.com/typestack/class-validator/issues/837



Patch:
diff --git a/src/decorator/typechecker/IsEnum.ts b/src/decorator/typechecker/IsEnum.ts
index 45b8c46..eb3d6b0 100644
--- a/src/decorator/typechecker/IsEnum.ts
+++ b/src/decorator/typechecker/IsEnum.ts
@@ -4,7 +4,7 @@ import { buildMessage, ValidateBy } from '../common/ValidateBy';
 export const IS_ENUM = 'isEnum';
 
 /**
- * Checks if a given value is an enum
+ * Checks if a given value is the member of the provided enum.
  */
 export function isEnum(value: unknown, entity: any): boolean {
   const enumValues = Object.keys(entity).map(k => entity[k]);
@@ -12,17 +12,26 @@ export function isEnum(value: unknown, entity: any): boolean {
 }
 
 /**
- * Checks if a given value is an enum
+ * Returns the possible values from an enum (both simple number indexed and string indexed enums).
+ */
+function validEnumValues(entity: any): string[] {
+  return Object.entries(entity)
+    .filter(([key, value]) => isNaN(parseInt(key)))
+    .map(([key, value]) => value as string);
+}
+
+/**
+ * Checks if a given value is the member of the provided enum.
  */
 export function IsEnum(entity: object, validationOptions?: ValidationOptions): PropertyDecorator {
   return ValidateBy(
     {
       name: IS_ENUM,
-      constraints: [entity],
+      constraints: [entity, validEnumValues(entity)],
       validator: {
         validate: (value, args): boolean => isEnum(value, args?.constraints[0]),
         defaultMessage: buildMessage(
-          eachPrefix => eachPrefix + '$property must be a valid enum value',
+          eachPrefix => eachPrefix + '$property must be one of the following values: $constraint2',
           validationOptions
         ),
       },


Test patch:
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index 45c4e4b..a96f45b 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -884,7 +884,12 @@ describe('IsArray', () => {
 });
 
 describe('IsEnum', () => {
-  enum MyEnum {
+  enum MyDefaultIndexedEnum {
+    First,
+    Second,
+  }
+
+  enum MyCustomIndexedEnum {
     First = 1,
     Second = 999,
   }
@@ -894,38 +899,43 @@ describe('IsEnum', () => {
     Second = 'second',
   }
 
-  const validValues = [MyEnum.First, MyEnum.Second];
+  const validValues = [MyCustomIndexedEnum.First, MyCustomIndexedEnum.Second];
   const validStringValues = [MyStringEnum.First, MyStringEnum.Second];
-  const invalidValues = [true, false, 0, {}, null, undefined, 'F2irst'];
+  const invalidValues = [true, false, 42, {}, null, undefined, 'F2irst'];
 
-  class MyClass {
-    @IsEnum(MyEnum)
-    someProperty: MyEnum;
+  class MyClassOne {
+    @IsEnum(MyDefaultIndexedEnum)
+    someProperty: MyDefaultIndexedEnum;
   }
 
-  class MyClass2 {
+  class MyClassTwo {
+    @IsEnum(MyCustomIndexedEnum)
+    someProperty: MyCustomIndexedEnum;
+  }
+
+  class MyClassThree {
     @IsEnum(MyStringEnum)
     someProperty: MyStringEnum;
   }
 
   it('should not fail if validator.validate said that its valid', () => {
-    return checkValidValues(new MyClass(), validValues);
+    return checkValidValues(new MyClassTwo(), validValues);
   });
 
   it('should not fail if validator.validate said that its valid (string enum)', () => {
-    return checkValidValues(new MyClass2(), validStringValues);
+    return checkValidValues(new MyClassThree(), validStringValues);
   });
 
   it('should fail if validator.validate said that its invalid', () => {
-    return checkInvalidValues(new MyClass(), invalidValues);
+    return checkInvalidValues(new MyClassTwo(), invalidValues);
   });
 
   it('should fail if validator.validate said that its invalid (string enum)', () => {
-    return checkInvalidValues(new MyClass2(), invalidValues);
+    return checkInvalidValues(new MyClassThree(), invalidValues);
   });
 
   it('should not fail if method in validator said that its valid', () => {
-    validValues.forEach(value => expect(isEnum(value, MyEnum)).toBeTruthy());
+    validValues.forEach(value => expect(isEnum(value, MyCustomIndexedEnum)).toBeTruthy());
   });
 
   it('should not fail if method in validator said that its valid (string enum)', () => {
@@ -933,23 +943,29 @@ describe('IsEnum', () => {
   });
 
   it('should fail if method in validator said that its invalid', () => {
-    invalidValues.forEach(value => expect(isEnum(value, MyEnum)).toBeFalsy());
+    invalidValues.forEach(value => expect(isEnum(value, MyCustomIndexedEnum)).toBeFalsy());
   });
 
   it('should fail if method in validator said that its invalid (string enum)', () => {
     invalidValues.forEach(value => expect(isEnum(value, MyStringEnum)).toBeFalsy());
   });
 
-  it('should return error object with proper data', () => {
+  it('should return error with proper message for default indexed enum', () => {
     const validationType = 'isEnum';
-    const message = 'someProperty must be a valid enum value';
-    return checkReturnedError(new MyClass(), invalidValues, validationType, message);
+    const message = 'someProperty must be one of the following values: 0, 1';
+    return checkReturnedError(new MyClassOne(), invalidValues, validationType, message);
+  });
+
+  it('should return error with proper message for custom indexed enum', () => {
+    const validationType = 'isEnum';
+    const message = 'someProperty must be one of the following values: 1, 999';
+    return checkReturnedError(new MyClassTwo(), invalidValues, validationType, message);
   });
 
-  it('should return error object with proper data (string enum)', () => {
+  it('should return error with proper message for string enum', () => {
     const validationType = 'isEnum';
-    const message = 'someProperty must be a valid enum value';
-    checkReturnedError(new MyClass2(), invalidValues, validationType, message);
+    const message = 'someProperty must be one of the following values: first, second';
+    return checkReturnedError(new MyClassThree(), invalidValues, validationType, message);
   });
 });