Skip to content

Commit f6fcdc5

Browse files
tarik02vlapo
authored andcommitted
feat: allow validate Map/Set (#365)
1 parent b29c818 commit f6fcdc5

File tree

9 files changed

+283
-0
lines changed

9 files changed

+283
-0
lines changed

Diff for: README.md

+40
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Class-validator works on both browser and node.js platforms.
1616
+ [Validation errors](#validation-errors)
1717
+ [Validation messages](#validation-messages)
1818
+ [Validating arrays](#validating-arrays)
19+
+ [Validating sets](#validating-sets)
20+
+ [Validating maps](#validating-maps)
1921
+ [Validating nested objects](#validating-nested-objects)
2022
+ [Inheriting Validation decorators](#inheriting-validation-decorators)
2123
+ [Conditional validation](#conditional-validation)
@@ -261,6 +263,44 @@ export class Post {
261263

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

266+
## Validating sets
267+
268+
If your field is a set and you want to perform validation of each item in the set you must specify a
269+
special `each: true` decorator option:
270+
271+
```typescript
272+
import {MinLength, MaxLength} from "class-validator";
273+
274+
export class Post {
275+
276+
@MaxLength(20, {
277+
each: true
278+
})
279+
tags: Set<string>;
280+
}
281+
```
282+
283+
This will validate each item in `post.tags` set.
284+
285+
## Validating maps
286+
287+
If your field is a map and you want to perform validation of each item in the map you must specify a
288+
special `each: true` decorator option:
289+
290+
```typescript
291+
import {MinLength, MaxLength} from "class-validator";
292+
293+
export class Post {
294+
295+
@MaxLength(20, {
296+
each: true
297+
})
298+
tags: Map<string, string>;
299+
}
300+
```
301+
302+
This will validate each item in `post.tags` map.
303+
264304
## Validating nested objects
265305

266306
If your object contains nested objects and you want the validator to perform their validation too, then you need to

Diff for: sample/sample8-es6-maps/Post.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, ValidateNested} from "../../src/decorator/decorators";
2+
import {Tag} from "./Tag";
3+
4+
export class Post {
5+
6+
@Length(10, 20, {
7+
message: "Incorrect length!"
8+
})
9+
title: string;
10+
11+
@ValidateNested()
12+
tags: Map<string, Tag>;
13+
14+
}

Diff for: sample/sample8-es6-maps/Tag.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate} from "../../src/decorator/decorators";
2+
3+
export class Tag {
4+
5+
@Length(10, 20, {
6+
message: "Tag value is too short or long"
7+
})
8+
value: string;
9+
10+
}

Diff for: sample/sample8-es6-maps/app.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {Validator} from "../../src/validation/Validator";
2+
import {Post} from "./Post";
3+
import {Tag} from "./Tag";
4+
5+
let validator = new Validator();
6+
7+
let tag1 = new Tag();
8+
tag1.value = "ja";
9+
10+
let tag2 = new Tag();
11+
tag2.value = "node.js";
12+
13+
let post1 = new Post();
14+
post1.title = "Hello world";
15+
post1.tags = new Map();
16+
post1.tags.set("tag1", tag1);
17+
post1.tags.set("tag2", tag2);
18+
19+
validator.validate(post1).then(result => {
20+
console.log("1. should not pass: ", result);
21+
});

Diff for: sample/sample9-es6-sets/Post.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate, ValidateNested} from "../../src/decorator/decorators";
2+
import {Tag} from "./Tag";
3+
4+
export class Post {
5+
6+
@Length(10, 20, {
7+
message: "Incorrect length!"
8+
})
9+
title: string;
10+
11+
@ValidateNested()
12+
tags: Set<Tag>;
13+
14+
}

Diff for: sample/sample9-es6-sets/Tag.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Contains, IsInt, Length, IsEmail, IsFQDN, IsDate} from "../../src/decorator/decorators";
2+
3+
export class Tag {
4+
5+
@Length(10, 20, {
6+
message: "Tag value is too short or long"
7+
})
8+
value: string;
9+
10+
}

Diff for: sample/sample9-es6-sets/app.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {Validator} from "../../src/validation/Validator";
2+
import {Post} from "./Post";
3+
import {Tag} from "./Tag";
4+
5+
let validator = new Validator();
6+
7+
let tag1 = new Tag();
8+
tag1.value = "ja";
9+
10+
let tag2 = new Tag();
11+
tag2.value = "node.js";
12+
13+
let post1 = new Post();
14+
post1.title = "Hello world";
15+
post1.tags = new Set();
16+
post1.tags.add(tag1);
17+
post1.tags.add(tag2);
18+
19+
validator.validate(post1).then(result => {
20+
console.log("1. should not pass: ", result);
21+
});

Diff for: src/validation/ValidationExecutor.ts

+19
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,25 @@ export class ValidationExecutor {
270270
this.execute(subValue, targetSchema, validationError.children);
271271
});
272272

273+
} else if (value instanceof Set) {
274+
let index = 0;
275+
value.forEach((subValue: any) => {
276+
const validationError = this.generateValidationError(value, subValue, index.toString());
277+
errors.push(validationError);
278+
279+
this.execute(subValue, targetSchema, validationError.children);
280+
281+
++index;
282+
});
283+
284+
} else if (value instanceof Map) {
285+
value.forEach((subValue: any, key: any) => {
286+
const validationError = this.generateValidationError(value, subValue, key.toString());
287+
errors.push(validationError);
288+
289+
this.execute(subValue, targetSchema, validationError.children);
290+
});
291+
273292
} else if (value instanceof Object) {
274293
this.execute(value, targetSchema, errors);
275294

Diff for: test/functional/nested-validation.spec.ts

+134
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,138 @@ describe("nested validation", function () {
138138

139139
});
140140

141+
it("should validate nested set", () => {
142+
143+
class MySubClass {
144+
@MinLength(5)
145+
name: string;
146+
}
147+
148+
class MyClass {
149+
@Contains("hello")
150+
title: string;
151+
152+
@ValidateNested()
153+
mySubClass: MySubClass;
154+
155+
@ValidateNested()
156+
mySubClasses: Set<MySubClass>;
157+
}
158+
159+
const model = new MyClass();
160+
model.title = "helo world";
161+
model.mySubClass = new MySubClass();
162+
model.mySubClass.name = "my";
163+
model.mySubClasses = new Set();
164+
165+
const submodel1 = new MySubClass();
166+
submodel1.name = "my";
167+
model.mySubClasses.add(submodel1);
168+
169+
const submodel2 = new MySubClass();
170+
submodel2.name = "not-short";
171+
model.mySubClasses.add(submodel2);
172+
173+
return validator.validate(model).then(errors => {
174+
errors.length.should.be.equal(3);
175+
176+
errors[0].target.should.be.equal(model);
177+
errors[0].property.should.be.equal("title");
178+
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
179+
errors[0].value.should.be.equal("helo world");
180+
181+
errors[1].target.should.be.equal(model);
182+
errors[1].property.should.be.equal("mySubClass");
183+
errors[1].value.should.be.equal(model.mySubClass);
184+
expect(errors[1].constraints).to.be.undefined;
185+
const subError1 = errors[1].children[0];
186+
subError1.target.should.be.equal(model.mySubClass);
187+
subError1.property.should.be.equal("name");
188+
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
189+
subError1.value.should.be.equal("my");
190+
191+
errors[2].target.should.be.equal(model);
192+
errors[2].property.should.be.equal("mySubClasses");
193+
errors[2].value.should.be.equal(model.mySubClasses);
194+
expect(errors[2].constraints).to.be.undefined;
195+
const subError2 = errors[2].children[0];
196+
subError2.target.should.be.equal(model.mySubClasses);
197+
subError2.value.should.be.equal(submodel1);
198+
subError2.property.should.be.equal("0");
199+
const subSubError = subError2.children[0];
200+
subSubError.target.should.be.equal(submodel1);
201+
subSubError.property.should.be.equal("name");
202+
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
203+
subSubError.value.should.be.equal("my");
204+
});
205+
206+
});
207+
208+
it("should validate nested map", () => {
209+
210+
class MySubClass {
211+
@MinLength(5)
212+
name: string;
213+
}
214+
215+
class MyClass {
216+
@Contains("hello")
217+
title: string;
218+
219+
@ValidateNested()
220+
mySubClass: MySubClass;
221+
222+
@ValidateNested()
223+
mySubClasses: Map<string, MySubClass>;
224+
}
225+
226+
const model = new MyClass();
227+
model.title = "helo world";
228+
model.mySubClass = new MySubClass();
229+
model.mySubClass.name = "my";
230+
model.mySubClasses = new Map();
231+
232+
const submodel1 = new MySubClass();
233+
submodel1.name = "my";
234+
model.mySubClasses.set("key1", submodel1);
235+
236+
const submodel2 = new MySubClass();
237+
submodel2.name = "not-short";
238+
model.mySubClasses.set("key2", submodel2);
239+
240+
return validator.validate(model).then(errors => {
241+
errors.length.should.be.equal(3);
242+
243+
errors[0].target.should.be.equal(model);
244+
errors[0].property.should.be.equal("title");
245+
errors[0].constraints.should.be.eql({contains: "title must contain a hello string"});
246+
errors[0].value.should.be.equal("helo world");
247+
248+
errors[1].target.should.be.equal(model);
249+
errors[1].property.should.be.equal("mySubClass");
250+
errors[1].value.should.be.equal(model.mySubClass);
251+
expect(errors[1].constraints).to.be.undefined;
252+
const subError1 = errors[1].children[0];
253+
subError1.target.should.be.equal(model.mySubClass);
254+
subError1.property.should.be.equal("name");
255+
subError1.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
256+
subError1.value.should.be.equal("my");
257+
258+
errors[2].target.should.be.equal(model);
259+
errors[2].property.should.be.equal("mySubClasses");
260+
errors[2].value.should.be.equal(model.mySubClasses);
261+
expect(errors[2].constraints).to.be.undefined;
262+
const subError2 = errors[2].children[0];
263+
subError2.target.should.be.equal(model.mySubClasses);
264+
subError2.value.should.be.equal(submodel1);
265+
subError2.property.should.be.equal("key1");
266+
const subSubError = subError2.children[0];
267+
subSubError.target.should.be.equal(submodel1);
268+
subSubError.property.should.be.equal("name");
269+
subSubError.constraints.should.be.eql({minLength: "name must be longer than or equal to 5 characters"});
270+
subSubError.value.should.be.equal("my");
271+
});
272+
273+
});
274+
141275
});

0 commit comments

Comments
 (0)