Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional matcher function for ArrayUnique decorator #830

Merged
merged 2 commits into from
Jan 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ isBoolean(value);
| `@ArrayNotEmpty()` | Checks if given array is not empty. |
| `@ArrayMinSize(min: number)` | Checks if the array's length is greater than or equal to the specified number. |
| `@ArrayMaxSize(max: number)` | Checks if the array's length is less or equal to the specified number. |
| `@ArrayUnique()` | Checks if all array's values are unique. Comparison for objects is reference-based. |
| `@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. |
| **Object validation decorators** |
| `@IsInstance(value: any)` | Checks if the property is an instance of the passed value. |
| **Other decorators** | |
Expand Down
24 changes: 16 additions & 8 deletions src/decorator/array/ArrayUnique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { ValidationOptions } from '../ValidationOptions';
import { buildMessage, ValidateBy } from '../common/ValidateBy';

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

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

if (identifier) {
array = array.map(o => (o != null ? identifier(o) : o));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you check for null here before calling the identifier function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check null before calling or check null in every identifier function. I just pick the former

}

const uniqueItems = array.filter((a, b, c) => c.indexOf(a) === b);
return array.length === uniqueItems.length;
}
Expand All @@ -18,18 +23,21 @@ export function arrayUnique(array: unknown): boolean {
* Checks if all array's values are unique. Comparison for objects is reference-based.
* If null or undefined is given then this function returns false.
*/
export function ArrayUnique(validationOptions?: ValidationOptions): PropertyDecorator {
export function ArrayUnique<T = any>(
identifierOrOptions?: ArrayUniqueIdentifier<T> | ValidationOptions,
validationOptions?: ValidationOptions
): PropertyDecorator {
const identifier = typeof identifierOrOptions === 'function' ? identifierOrOptions : undefined;
const options = typeof identifierOrOptions !== 'function' ? identifierOrOptions : validationOptions;

return ValidateBy(
{
name: ARRAY_UNIQUE,
validator: {
validate: (value, args): boolean => arrayUnique(value),
defaultMessage: buildMessage(
eachPrefix => eachPrefix + "All $property's elements must be unique",
validationOptions
),
validate: (value, args): boolean => arrayUnique(value, identifier),
defaultMessage: buildMessage(eachPrefix => eachPrefix + "All $property's elements must be unique", options),
},
},
validationOptions
options
);
}
45 changes: 45 additions & 0 deletions test/functional/validation-functions-and-decorators.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4365,6 +4365,7 @@ describe('ArrayUnique', () => {
['world', 'hello', 'superman'],
['world', 'superman', 'hello'],
['superman', 'world', 'hello'],
['1', '2', null, undefined],
];
const invalidValues: any[] = [
null,
Expand Down Expand Up @@ -4402,6 +4403,50 @@ describe('ArrayUnique', () => {
});
});

describe('ArrayUnique with identifier', () => {
const identifier = o => o.name;
const validValues = [
['world', 'hello', 'superman'],
['world', 'superman', 'hello'],
['superman', 'world', 'hello'],
['1', '2', null, undefined],
].map(list => list.map(name => ({ name })));
const invalidValues: any[] = [
null,
undefined,
['world', 'hello', 'hello'],
['world', 'hello', 'world'],
['1', '1', '1'],
].map(list => list?.map(name => (name != null ? { name } : name)));

class MyClass {
@ArrayUnique(identifier)
someProperty: { name: string }[];
}

it('should not fail if validator.validate said that its valid', () => {
return checkValidValues(new MyClass(), validValues);
});

it('should fail if validator.validate said that its invalid', () => {
return checkInvalidValues(new MyClass(), invalidValues);
});

it('should not fail if method in validator said that its valid', () => {
validValues.forEach(value => expect(arrayUnique(value, identifier)).toBeTruthy());
});

it('should fail if method in validator said that its invalid', () => {
invalidValues.forEach(value => expect(arrayUnique(value, identifier)).toBeFalsy());
});

it('should return error object with proper data', () => {
const validationType = 'arrayUnique';
const message = "All someProperty's elements must be unique";
return checkReturnedError(new MyClass(), invalidValues, validationType, message);
});
});

describe('isInstance', () => {
class MySubClass {
// Empty
Expand Down