From 5f2312a94b7bd9e12b3fb54d31bf56fdf8fa6627 Mon Sep 17 00:00:00 2001
From: MamadTvl <mtavallai80@gmail.com>
Date: Sun, 14 Apr 2024 22:19:49 +0330
Subject: [PATCH] feat: add decorator to check if two properties match #486

---
 README.md                                     |  1 +
 src/decorator/common/DoesMatch.ts             | 20 +++++++++++
 src/decorator/decorators.ts                   |  1 +
 ...alidation-functions-and-decorators.spec.ts | 33 +++++++++++++++++++
 4 files changed, 55 insertions(+)
 create mode 100644 src/decorator/common/DoesMatch.ts

diff --git a/README.md b/README.md
index 886712dd76..5b8b1f8edc 100644
--- a/README.md
+++ b/README.md
@@ -805,6 +805,7 @@ isBoolean(value);
 | `@IsNotEmpty()`                                        | Checks if given value is not empty (!== '', !== null, !== undefined).                                                                                                                                 |
 | `@IsIn(values: any[])`                                 | Checks if value is in an array of allowed values.                                                                                                                                                     |
 | `@IsNotIn(values: any[])`                              | Checks if value is not in an array of disallowed values.                                                                                                                                              |
+| `@DoesMatch(condition: (obj, value) => boolean)`                              | Checks if a given value follows a custom condition.                                                                                                                                              |
 | **Type validation decorators**                         |                                                                                                                                                                                                       |
 | `@IsBoolean()`                                         | Checks if a value is a boolean.                                                                                                                                                                       |
 | `@IsDate()`                                            | Checks if the value is a date.                                                                                                                                                                        |
diff --git a/src/decorator/common/DoesMatch.ts b/src/decorator/common/DoesMatch.ts
new file mode 100644
index 0000000000..40fb07c8b1
--- /dev/null
+++ b/src/decorator/common/DoesMatch.ts
@@ -0,0 +1,20 @@
+import { ValidationOptions } from '../ValidationOptions';
+import { ValidateBy, buildMessage } from './ValidateBy';
+
+export const DOES_MATCH = 'doesMatch';
+
+/**
+ * Checks if a given value follows a custom condition.
+ */
+export function DoesMatch(
+  condition: (object: any, value: any) => boolean,
+  validationOptions?: ValidationOptions
+): PropertyDecorator {
+  return ValidateBy({
+    name: DOES_MATCH,
+    validator: {
+      validate: (value, args): boolean => condition(args?.object, value),
+      defaultMessage: buildMessage(eachPrefix => eachPrefix + '$property does not match', validationOptions),
+    },
+  });
+}
diff --git a/src/decorator/decorators.ts b/src/decorator/decorators.ts
index d449e9301a..b5e3b71ba7 100644
--- a/src/decorator/decorators.ts
+++ b/src/decorator/decorators.ts
@@ -23,6 +23,7 @@ export * from './common/IsEmpty';
 export * from './common/IsNotEmpty';
 export * from './common/IsIn';
 export * from './common/IsNotIn';
+export * from './common/DoesMatch';
 
 // -------------------------------------------------------------------------
 // Number checkers
diff --git a/test/functional/validation-functions-and-decorators.spec.ts b/test/functional/validation-functions-and-decorators.spec.ts
index 78ceddcd6d..caf60f812d 100644
--- a/test/functional/validation-functions-and-decorators.spec.ts
+++ b/test/functional/validation-functions-and-decorators.spec.ts
@@ -193,6 +193,7 @@ import {
   isTaxId,
   IsTaxId,
   IsISO4217CurrencyCode,
+  DoesMatch,
 } from '../../src/decorator/decorators';
 import { Validator } from '../../src/validation/Validator';
 import { ValidatorOptions } from '../../src/validation/ValidatorOptions';
@@ -519,6 +520,38 @@ describe('IsNotIn', () => {
   });
 });
 
+describe('DoesMatch', () => {
+  const validValues = ['foobar'];
+  const invalidValues = ['bar'];
+  class MyClass {
+    @DoesMatch((obj, value) => obj.propertyToMatch === value)
+    someProperty: string;
+
+    @IsString()
+    propertyToMatch: string;
+  }
+
+  it('should not fail if validator.validate said that its valid', () => {
+    const obj = new MyClass();
+    obj.propertyToMatch = 'foobar';
+    return checkValidValues(obj, validValues);
+  });
+
+  it('should fail if validator.validate said that its invalid', () => {
+    const obj = new MyClass();
+    obj.propertyToMatch = 'foobar';
+    return checkInvalidValues(obj, invalidValues);
+  });
+
+  it('should return error object with proper data', () => {
+    const obj = new MyClass();
+    obj.propertyToMatch = 'foobar';
+    const validationType = 'doesMatch';
+    const message = 'someProperty does not match';
+    return checkReturnedError(obj, invalidValues, validationType, message);
+  });
+});
+
 // -------------------------------------------------------------------------
 // Specifications: type check
 // -------------------------------------------------------------------------