Skip to content

Commit 90b3ae9

Browse files
authored
feat: add optional matcher function for ArrayUnique decorator (typestack#830)
1 parent 6d42226 commit 90b3ae9

File tree

3 files changed

+62
-9
lines changed

3 files changed

+62
-9
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ isBoolean(value);
897897
| `@ArrayNotEmpty()` | Checks if given array is not empty. |
898898
| `@ArrayMinSize(min: number)` | Checks if the array's length is greater than or equal to the specified number. |
899899
| `@ArrayMaxSize(max: number)` | Checks if the array's length is less or equal to the specified number. |
900-
| `@ArrayUnique()` | Checks if all array's values are unique. Comparison for objects is reference-based. |
900+
| `@ArrayUnique(identifier?: (o) => any)` | Checks if all array's values are unique. Comparison for objects is reference-based. Optional function can be speciefied which return value will be used for the comparsion. |
901901
| **Object validation decorators** |
902902
| `@IsInstance(value: any)` | Checks if the property is an instance of the passed value. |
903903
| **Other decorators** | |

src/decorator/array/ArrayUnique.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import { ValidationOptions } from '../ValidationOptions';
22
import { buildMessage, ValidateBy } from '../common/ValidateBy';
33

44
export const ARRAY_UNIQUE = 'arrayUnique';
5+
export type ArrayUniqueIdentifier<T = any> = (o: T) => any;
56

67
/**
78
* Checks if all array's values are unique. Comparison for objects is reference-based.
89
* If null or undefined is given then this function returns false.
910
*/
10-
export function arrayUnique(array: unknown): boolean {
11+
export function arrayUnique(array: unknown[], identifier?: ArrayUniqueIdentifier): boolean {
1112
if (!(array instanceof Array)) return false;
1213

14+
if (identifier) {
15+
array = array.map(o => (o != null ? identifier(o) : o));
16+
}
17+
1318
const uniqueItems = array.filter((a, b, c) => c.indexOf(a) === b);
1419
return array.length === uniqueItems.length;
1520
}
@@ -18,18 +23,21 @@ export function arrayUnique(array: unknown): boolean {
1823
* Checks if all array's values are unique. Comparison for objects is reference-based.
1924
* If null or undefined is given then this function returns false.
2025
*/
21-
export function ArrayUnique(validationOptions?: ValidationOptions): PropertyDecorator {
26+
export function ArrayUnique<T = any>(
27+
identifierOrOptions?: ArrayUniqueIdentifier<T> | ValidationOptions,
28+
validationOptions?: ValidationOptions
29+
): PropertyDecorator {
30+
const identifier = typeof identifierOrOptions === 'function' ? identifierOrOptions : undefined;
31+
const options = typeof identifierOrOptions !== 'function' ? identifierOrOptions : validationOptions;
32+
2233
return ValidateBy(
2334
{
2435
name: ARRAY_UNIQUE,
2536
validator: {
26-
validate: (value, args): boolean => arrayUnique(value),
27-
defaultMessage: buildMessage(
28-
eachPrefix => eachPrefix + "All $property's elements must be unique",
29-
validationOptions
30-
),
37+
validate: (value, args): boolean => arrayUnique(value, identifier),
38+
defaultMessage: buildMessage(eachPrefix => eachPrefix + "All $property's elements must be unique", options),
3139
},
3240
},
33-
validationOptions
41+
options
3442
);
3543
}

test/functional/validation-functions-and-decorators.spec.ts

+45
Original file line numberDiff line numberDiff line change
@@ -4365,6 +4365,7 @@ describe('ArrayUnique', () => {
43654365
['world', 'hello', 'superman'],
43664366
['world', 'superman', 'hello'],
43674367
['superman', 'world', 'hello'],
4368+
['1', '2', null, undefined],
43684369
];
43694370
const invalidValues: any[] = [
43704371
null,
@@ -4402,6 +4403,50 @@ describe('ArrayUnique', () => {
44024403
});
44034404
});
44044405

4406+
describe('ArrayUnique with identifier', () => {
4407+
const identifier = o => o.name;
4408+
const validValues = [
4409+
['world', 'hello', 'superman'],
4410+
['world', 'superman', 'hello'],
4411+
['superman', 'world', 'hello'],
4412+
['1', '2', null, undefined],
4413+
].map(list => list.map(name => ({ name })));
4414+
const invalidValues: any[] = [
4415+
null,
4416+
undefined,
4417+
['world', 'hello', 'hello'],
4418+
['world', 'hello', 'world'],
4419+
['1', '1', '1'],
4420+
].map(list => list?.map(name => (name != null ? { name } : name)));
4421+
4422+
class MyClass {
4423+
@ArrayUnique(identifier)
4424+
someProperty: { name: string }[];
4425+
}
4426+
4427+
it('should not fail if validator.validate said that its valid', () => {
4428+
return checkValidValues(new MyClass(), validValues);
4429+
});
4430+
4431+
it('should fail if validator.validate said that its invalid', () => {
4432+
return checkInvalidValues(new MyClass(), invalidValues);
4433+
});
4434+
4435+
it('should not fail if method in validator said that its valid', () => {
4436+
validValues.forEach(value => expect(arrayUnique(value, identifier)).toBeTruthy());
4437+
});
4438+
4439+
it('should fail if method in validator said that its invalid', () => {
4440+
invalidValues.forEach(value => expect(arrayUnique(value, identifier)).toBeFalsy());
4441+
});
4442+
4443+
it('should return error object with proper data', () => {
4444+
const validationType = 'arrayUnique';
4445+
const message = "All someProperty's elements must be unique";
4446+
return checkReturnedError(new MyClass(), invalidValues, validationType, message);
4447+
});
4448+
});
4449+
44054450
describe('isInstance', () => {
44064451
class MySubClass {
44074452
// Empty

0 commit comments

Comments
 (0)