Skip to content

Commit

Permalink
feat: allow validate Map/Set (#365)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarik02 authored and vlapo committed Jul 17, 2019
1 parent b29c818 commit f6fcdc5
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 0 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Class-validator works on both browser and node.js platforms.
+ [Validation errors](#validation-errors)
+ [Validation messages](#validation-messages)
+ [Validating arrays](#validating-arrays)
+ [Validating sets](#validating-sets)
+ [Validating maps](#validating-maps)
+ [Validating nested objects](#validating-nested-objects)
+ [Inheriting Validation decorators](#inheriting-validation-decorators)
+ [Conditional validation](#conditional-validation)
Expand Down Expand Up @@ -261,6 +263,44 @@ export class Post {

This will validate each item in `post.tags` array.

## Validating sets

If your field is a set and you want to perform validation of each item in the set you must specify a
special `each: true` decorator option:

```typescript
import {MinLength, MaxLength} from "class-validator";

export class Post {

@MaxLength(20, {
each: true
})
tags: Set<string>;
}
```

This will validate each item in `post.tags` set.

## Validating maps

If your field is a map and you want to perform validation of each item in the map you must specify a
special `each: true` decorator option:

```typescript
import {MinLength, MaxLength} from "class-validator";

export class Post {

@MaxLength(20, {
each: true
})
tags: Map<string, string>;
}
```

This will validate each item in `post.tags` map.

## Validating nested objects

If your object contains nested objects and you want the validator to perform their validation too, then you need to
Expand Down
14 changes: 14 additions & 0 deletions sample/sample8-es6-maps/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, ValidateNested} from "../../src/decorator/decorators";
import {Tag} from "./Tag";

export class Post {

@Length(10, 20, {
message: "Incorrect length!"
})
title: string;

@ValidateNested()
tags: Map<string, Tag>;

}
10 changes: 10 additions & 0 deletions sample/sample8-es6-maps/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate} from "../../src/decorator/decorators";

export class Tag {

@Length(10, 20, {
message: "Tag value is too short or long"
})
value: string;

}
21 changes: 21 additions & 0 deletions sample/sample8-es6-maps/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Validator} from "../../src/validation/Validator";
import {Post} from "./Post";
import {Tag} from "./Tag";

let validator = new Validator();

let tag1 = new Tag();
tag1.value = "ja";

let tag2 = new Tag();
tag2.value = "node.js";

let post1 = new Post();
post1.title = "Hello world";
post1.tags = new Map();
post1.tags.set("tag1", tag1);
post1.tags.set("tag2", tag2);

validator.validate(post1).then(result => {
console.log("1. should not pass: ", result);
});
14 changes: 14 additions & 0 deletions sample/sample9-es6-sets/Post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, ValidateNested} from "../../src/decorator/decorators";
import {Tag} from "./Tag";

export class Post {

@Length(10, 20, {
message: "Incorrect length!"
})
title: string;

@ValidateNested()
tags: Set<Tag>;

}
10 changes: 10 additions & 0 deletions sample/sample9-es6-sets/Tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate} from "../../src/decorator/decorators";

export class Tag {

@Length(10, 20, {
message: "Tag value is too short or long"
})
value: string;

}
21 changes: 21 additions & 0 deletions sample/sample9-es6-sets/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {Validator} from "../../src/validation/Validator";
import {Post} from "./Post";
import {Tag} from "./Tag";

let validator = new Validator();

let tag1 = new Tag();
tag1.value = "ja";

let tag2 = new Tag();
tag2.value = "node.js";

let post1 = new Post();
post1.title = "Hello world";
post1.tags = new Set();
post1.tags.add(tag1);
post1.tags.add(tag2);

validator.validate(post1).then(result => {
console.log("1. should not pass: ", result);
});
19 changes: 19 additions & 0 deletions src/validation/ValidationExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,25 @@ export class ValidationExecutor {
this.execute(subValue, targetSchema, validationError.children);
});

} else if (value instanceof Set) {
let index = 0;
value.forEach((subValue: any) => {
const validationError = this.generateValidationError(value, subValue, index.toString());
errors.push(validationError);

this.execute(subValue, targetSchema, validationError.children);

++index;
});

} else if (value instanceof Map) {
value.forEach((subValue: any, key: any) => {
const validationError = this.generateValidationError(value, subValue, key.toString());
errors.push(validationError);

this.execute(subValue, targetSchema, validationError.children);
});

} else if (value instanceof Object) {
this.execute(value, targetSchema, errors);

Expand Down
134 changes: 134 additions & 0 deletions test/functional/nested-validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,138 @@ describe("nested validation", function () {

});

it("should validate nested set", () => {

class MySubClass {
@MinLength(5)
name: string;
}

class MyClass {
@Contains("hello")
title: string;

@ValidateNested()
mySubClass: MySubClass;

@ValidateNested()
mySubClasses: Set<MySubClass>;
}

const model = new MyClass();
model.title = "helo world";
model.mySubClass = new MySubClass();
model.mySubClass.name = "my";
model.mySubClasses = new Set();

const submodel1 = new MySubClass();
submodel1.name = "my";
model.mySubClasses.add(submodel1);

const submodel2 = new MySubClass();
submodel2.name = "not-short";
model.mySubClasses.add(submodel2);

return validator.validate(model).then(errors => {
errors.length.should.be.equal(3);

errors[0].target.should.be.equal(model);
errors[0].property.should.be.equal("title");
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
errors[0].value.should.be.equal("helo world");

errors[1].target.should.be.equal(model);
errors[1].property.should.be.equal("mySubClass");
errors[1].value.should.be.equal(model.mySubClass);
expect(errors[1].constraints).to.be.undefined;
const subError1 = errors[1].children[0];
subError1.target.should.be.equal(model.mySubClass);
subError1.property.should.be.equal("name");
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subError1.value.should.be.equal("my");

errors[2].target.should.be.equal(model);
errors[2].property.should.be.equal("mySubClasses");
errors[2].value.should.be.equal(model.mySubClasses);
expect(errors[2].constraints).to.be.undefined;
const subError2 = errors[2].children[0];
subError2.target.should.be.equal(model.mySubClasses);
subError2.value.should.be.equal(submodel1);
subError2.property.should.be.equal("0");
const subSubError = subError2.children[0];
subSubError.target.should.be.equal(submodel1);
subSubError.property.should.be.equal("name");
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subSubError.value.should.be.equal("my");
});

});

it("should validate nested map", () => {

class MySubClass {
@MinLength(5)
name: string;
}

class MyClass {
@Contains("hello")
title: string;

@ValidateNested()
mySubClass: MySubClass;

@ValidateNested()
mySubClasses: Map<string, MySubClass>;
}

const model = new MyClass();
model.title = "helo world";
model.mySubClass = new MySubClass();
model.mySubClass.name = "my";
model.mySubClasses = new Map();

const submodel1 = new MySubClass();
submodel1.name = "my";
model.mySubClasses.set("key1", submodel1);

const submodel2 = new MySubClass();
submodel2.name = "not-short";
model.mySubClasses.set("key2", submodel2);

return validator.validate(model).then(errors => {
errors.length.should.be.equal(3);

errors[0].target.should.be.equal(model);
errors[0].property.should.be.equal("title");
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
errors[0].value.should.be.equal("helo world");

errors[1].target.should.be.equal(model);
errors[1].property.should.be.equal("mySubClass");
errors[1].value.should.be.equal(model.mySubClass);
expect(errors[1].constraints).to.be.undefined;
const subError1 = errors[1].children[0];
subError1.target.should.be.equal(model.mySubClass);
subError1.property.should.be.equal("name");
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subError1.value.should.be.equal("my");

errors[2].target.should.be.equal(model);
errors[2].property.should.be.equal("mySubClasses");
errors[2].value.should.be.equal(model.mySubClasses);
expect(errors[2].constraints).to.be.undefined;
const subError2 = errors[2].children[0];
subError2.target.should.be.equal(model.mySubClasses);
subError2.value.should.be.equal(submodel1);
subError2.property.should.be.equal("key1");
const subSubError = subError2.children[0];
subSubError.target.should.be.equal(submodel1);
subSubError.property.should.be.equal("name");
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
subSubError.value.should.be.equal("my");
});

});

});

0 comments on commit f6fcdc5

Please sign in to comment.