7dd529dae9f35373b77257f2dc35cbffa66fca99
[nit.git] / lib / popcorn / pop_validation.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Quick and easy validation framework for Json inputs
18 #
19 # Validators can be used in Popcorn apps to valid your json inputs before
20 # data processing and persistence.
21 #
22 # Here an example with a Book management app. We use an ObjectValidator to validate
23 # the books passed to the API in the `POST /books` handler.
24 #
25 # ~~~
26 # import popcorn
27 # import serialization
28 #
29 # # Serializable book representation.
30 # class Book
31 # super Jsonable
32 #
33 # # Book ISBN
34 # var isbn: String
35 #
36 # # Book title
37 # var title: String
38 #
39 # # Book image (optional)
40 # var img: nullable String
41 #
42 # # Book price
43 # var price: Float
44 # end
45 #
46 # class BookValidator
47 # super ObjectValidator
48 #
49 # redef init do
50 # add new ISBNField("isbn")
51 # add new StringField("title", min_size=1, max_size=255)
52 # add new StringField("img", required=false)
53 # add new FloatField("price", min=0.0, max=999.0)
54 # end
55 # end
56 #
57 # class BookHandler
58 # super Handler
59 #
60 # # Insert a new Book
61 # redef fun post(req, res) do
62 # var validator = new BookValidator
63 # if not validator.validate(req.body) then
64 # res.json_error(validator.validation, 400)
65 # return
66 # end
67 # # TODO data persistence
68 # end
69 # end
70 # ~~~
71 module pop_validation
72
73 import json
74
75 # The base class of all validators
76 abstract class DocumentValidator
77
78 # Validation result
79 #
80 # Accessible to the client after the `validate` method has been called.
81 var validation: ValidationResult is noinit
82
83 # Validate the `document` input
84 #
85 # Result of the validation can be found in the `validation` attribute.
86 fun validate(document: String): Bool do
87 validation = new ValidationResult
88 return true
89 end
90 end
91
92 # Validation Result representation
93 #
94 # Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
95 #
96 # Errors messages are grouped into *scopes*. A scope is a string that specify wich
97 # field or document the error message is related to.
98 #
99 # See `HttpResponse::json_error`.
100 class ValidationResult
101 super Jsonable
102
103 # Object parsed during validation
104 #
105 # Can be used as a quick way to access the parsed JsonObject instead of
106 # reparsing it during the answer.
107 #
108 # See `ObjectValidator`.
109 var object: nullable JsonObject = null is writable
110
111 # Array parsed during validation
112 #
113 # Can be used as a quick way to access the parsed JsonArray instead of
114 # reparsing it during the answer.
115 #
116 # See `ArrayValidator`.
117 var array: nullable JsonArray = null is writable
118
119 # Errors found during validation
120 #
121 # Errors are grouped by scope.
122 var errors = new HashMap[String, Array[String]]
123
124 # Generate a new error `message` into `scope`
125 fun add_error(scope, message: String) do
126 if not errors.has_key(scope) then
127 errors[scope] = new Array[String]
128 end
129 errors[scope].add message
130 end
131
132 # Get the errors for `scope`
133 fun error(scope: String): Array[String] do
134 if not errors.has_key(scope) then
135 return new Array[String]
136 end
137 return errors[scope]
138 end
139
140 # Does `self` contains `errors`?
141 fun has_error: Bool do return errors.not_empty
142
143 # Render self as a JsonObject
144 fun json: JsonObject do
145 var obj = new JsonObject
146 obj["has_error"] = has_error
147 var e = new JsonObject
148 for k, v in errors do
149 e[k] = new JsonArray.from(v)
150 end
151 obj["errors"] = e
152 return obj
153 end
154
155 redef fun serialize_to(v) do json.serialize_to(v)
156
157 # Returns the validation result as a pretty formated string
158 fun to_pretty_string: String do
159 var b = new Buffer
160 if not has_error then
161 b.append "Everything is correct\n"
162 else
163 b.append "There is errors\n\n"
164 for k, v in errors do
165 b.append "{k}:\n"
166 for vv in v do
167 b.append "\t{vv}\n"
168 end
169 b.append "\n"
170 end
171 end
172 return b.write_to_string
173 end
174 end
175
176 # Check a JsonObject
177 # ~~~
178 # var validator = new ObjectValidator
179 # validator.add new RequiredField("id", required = true)
180 # validator.add new StringField("login", min_size=4)
181 # validator.add new IntField("age", min=0, max=100)
182 # assert not validator.validate("""{}""")
183 # assert not validator.validate("""[]""")
184 # assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
185 # ~~~
186 class ObjectValidator
187 super DocumentValidator
188
189 # Validators to apply on the object
190 var validators = new Array[FieldValidator]
191
192 redef fun validate(document) do
193 super
194 var json = document.parse_json
195 if json == null then
196 validation.add_error("document", "Expected JsonObject got `null`")
197 return false
198 end
199 return validate_json(json)
200 end
201
202 # Validate a Jsonable input
203 fun validate_json(json: Jsonable): Bool do
204 if not json isa JsonObject then
205 validation.add_error("document", "Expected JsonObject got `{json.class_name}`")
206 return false
207 end
208 validation.object = json
209 for validator in validators do
210 var res = validator.validate_field(self, json)
211 if not res then return false
212 end
213 return true
214 end
215
216 # Add a validator
217 fun add(validator: FieldValidator) do validators.add validator
218 end
219
220 # Check a JsonArray
221 # ~~~
222 # var validator = new ArrayValidator
223 # assert not validator.validate("""{}""")
224 # assert validator.validate("""[]""")
225 # assert validator.validate("""[ "id", 10, {} ]""")
226 #
227 # validator = new ArrayValidator(allow_empty=false)
228 # assert not validator.validate("""[]""")
229 # assert validator.validate("""[ "id", 10, {} ]""")
230 #
231 # validator = new ArrayValidator(length=3)
232 # assert not validator.validate("""[]""")
233 # assert validator.validate("""[ "id", 10, {} ]""")
234 # ~~~
235 class ArrayValidator
236 super DocumentValidator
237
238 # Allow empty arrays (default: true)
239 var allow_empty: nullable Bool
240
241 # Check array length (default: no check)
242 var length: nullable Int
243
244 redef fun validate(document) do
245 super
246 var json = document.parse_json
247 if json == null then
248 validation.add_error("document", "Expected JsonArray got `null`")
249 return false
250 end
251 return validate_json(json)
252 end
253
254 # Validate a Jsonable input
255 fun validate_json(json: Jsonable): Bool do
256 if not json isa JsonArray then
257 validation.add_error("document", "Expected JsonArray got `{json.class_name}`")
258 return false
259 end
260 validation.array = json
261 var allow_empty = self.allow_empty
262 if json.is_empty and (allow_empty != null and not allow_empty) then
263 validation.add_error("document", "Cannot be empty")
264 return false
265 end
266 var length = self.length
267 if length != null and json.length != length then
268 validation.add_error("document", "Array length must be exactly `{length}`")
269 return false
270 end
271
272 return true
273 end
274 end
275
276 # Something that can validate a JsonObject field
277 abstract class FieldValidator
278
279 # Field to validate
280 var field: String
281
282 # Validate `field` in `obj`
283 fun validate_field(v: ObjectValidator, obj: JsonObject): Bool is abstract
284 end
285
286 # Check if a field exists
287 #
288 # ~~~
289 # var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
290 # var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
291 # var json3 = """{ "field1": "", "field2": "foo" }"""
292 #
293 # var validator = new ObjectValidator
294 # validator.add new RequiredField("field1")
295 # validator.add new RequiredField("field2")
296 # validator.add new RequiredField("field3")
297 # validator.add new RequiredField("field4", required=false)
298 #
299 # assert validator.validate(json1)
300 # assert validator.validate(json2)
301 # assert not validator.validate(json3)
302 # assert validator.validation.error("field3") == ["Required field"]
303 # ~~~
304 class RequiredField
305 super FieldValidator
306
307 # Is this field required?
308 var required: nullable Bool
309
310 redef fun validate_field(v, obj) do
311 var required = self.required
312 if (required != null and required or required == null) and not obj.has_key(field) then
313 v.validation.add_error(field, "Required field")
314 return false
315 end
316 return true
317 end
318 end
319
320 # Check if a field is a String
321 #
322 # `min_size` and `max_size` are optional
323 #
324 # ~~~
325 # var validator = new ObjectValidator
326 # validator.add new StringField("field", required=false)
327 # assert validator.validate("""{}""")
328 #
329 # validator = new ObjectValidator
330 # validator.add new StringField("field")
331 # assert not validator.validate("""{}""")
332 # assert not validator.validate("""{ "field": 10 }""")
333 #
334 # validator = new ObjectValidator
335 # validator.add new StringField("field", min_size=3)
336 # assert validator.validate("""{ "field": "foo" }""")
337 # assert not validator.validate("""{ "field": "fo" }""")
338 # assert not validator.validate("""{ "field": "" }""")
339 #
340 # validator = new ObjectValidator
341 # validator.add new StringField("field", max_size=3)
342 # assert validator.validate("""{ "field": "foo" }""")
343 # assert not validator.validate("""{ "field": "fooo" }""")
344 #
345 # validator = new ObjectValidator
346 # validator.add new StringField("field", min_size=3, max_size=5)
347 # assert not validator.validate("""{ "field": "fo" }""")
348 # assert validator.validate("""{ "field": "foo" }""")
349 # assert validator.validate("""{ "field": "foooo" }""")
350 # assert not validator.validate("""{ "field": "fooooo" }""")
351 # ~~~
352 class StringField
353 super RequiredField
354
355 # String min size (default: not checked)
356 var min_size: nullable Int
357
358 # String max size (default: not checked)
359 var max_size: nullable Int
360
361 redef fun validate_field(v, obj) do
362 if not super then return false
363 var val = obj.get_or_null(field)
364 if val == null then
365 if required == null or required == true then
366 v.validation.add_error(field, "Expected String got `null`")
367 return false
368 else
369 return true
370 end
371 end
372 if not val isa String then
373 v.validation.add_error(field, "Expected String got `{val.class_name}`")
374 return false
375 end
376 var min_size = self.min_size
377 if min_size != null and val.length < min_size then
378 v.validation.add_error(field, "Must be at least `{min_size} characters long`")
379 return false
380 end
381 var max_size = self.max_size
382 if max_size != null and val.length > max_size then
383 v.validation.add_error(field, "Must be at max `{max_size} characters long`")
384 return false
385 end
386 return true
387 end
388 end
389
390 # Check if a field is an Int
391 #
392 # ~~~
393 # var validator = new ObjectValidator
394 # validator.add new IntField("field", required=false)
395 # assert validator.validate("""{}""")
396 #
397 # validator = new ObjectValidator
398 # validator.add new IntField("field")
399 # assert not validator.validate("""{}""")
400 # assert not validator.validate("""{ "field": "foo" }""")
401 # assert validator.validate("""{ "field": 10 }""")
402 #
403 # validator = new ObjectValidator
404 # validator.add new IntField("field", min=3)
405 # assert validator.validate("""{ "field": 3 }""")
406 # assert not validator.validate("""{ "field": 2 }""")
407 #
408 # validator = new ObjectValidator
409 # validator.add new IntField("field", max=3)
410 # assert validator.validate("""{ "field": 3 }""")
411 # assert not validator.validate("""{ "field": 4 }""")
412 #
413 # validator = new ObjectValidator
414 # validator.add new IntField("field", min=3, max=5)
415 # assert not validator.validate("""{ "field": 2 }""")
416 # assert validator.validate("""{ "field": 3 }""")
417 # assert validator.validate("""{ "field": 5 }""")
418 # assert not validator.validate("""{ "field": 6 }""")
419 # ~~~
420 class IntField
421 super RequiredField
422
423 # Min value (default: not checked)
424 var min: nullable Int
425
426 # Max value (default: not checked)
427 var max: nullable Int
428
429 redef fun validate_field(v, obj) do
430 if not super then return false
431 var val = obj.get_or_null(field)
432 if val == null then
433 if required == null or required == true then
434 v.validation.add_error(field, "Expected Int got `null`")
435 return false
436 else
437 return true
438 end
439 end
440 if not val isa Int then
441 v.validation.add_error(field, "Expected Int got `{val.class_name}`")
442 return false
443 end
444 var min = self.min
445 if min != null and val < min then
446 v.validation.add_error(field, "Must be greater or equal to `{min}`")
447 return false
448 end
449 var max = self.max
450 if max != null and val > max then
451 v.validation.add_error(field, "Must be smaller or equal to `{max}`")
452 return false
453 end
454 return true
455 end
456 end
457
458 # Check if a field is a Float
459 #
460 # ~~~
461 # var validator = new ObjectValidator
462 # validator.add new FloatField("field", required=false)
463 # assert validator.validate("""{}""")
464 #
465 # validator = new ObjectValidator
466 # validator.add new FloatField("field")
467 # assert not validator.validate("""{}""")
468 # assert not validator.validate("""{ "field": "foo" }""")
469 # assert validator.validate("""{ "field": 10.5 }""")
470 #
471 # validator = new ObjectValidator
472 # validator.add new FloatField("field", min=3.0)
473 # assert validator.validate("""{ "field": 3.0 }""")
474 # assert not validator.validate("""{ "field": 2.0 }""")
475 #
476 # validator = new ObjectValidator
477 # validator.add new FloatField("field", max=3.0)
478 # assert validator.validate("""{ "field": 3.0 }""")
479 # assert not validator.validate("""{ "field": 4.0 }""")
480 #
481 # validator = new ObjectValidator
482 # validator.add new FloatField("field", min=3.0, max=5.0)
483 # assert not validator.validate("""{ "field": 2.0 }""")
484 # assert validator.validate("""{ "field": 3.0 }""")
485 # assert validator.validate("""{ "field": 5.0 }""")
486 # assert not validator.validate("""{ "field": 6.0 }""")
487 # ~~~
488 class FloatField
489 super RequiredField
490
491 # Min value (default: not checked)
492 var min: nullable Float
493
494 # Max value (default: not checked)
495 var max: nullable Float
496
497 redef fun validate_field(v, obj) do
498 if not super then return false
499 var val = obj.get_or_null(field)
500 if val == null then
501 if required == null or required == true then
502 v.validation.add_error(field, "Expected Float got `null`")
503 return false
504 else
505 return true
506 end
507 end
508 if not val isa Float then
509 v.validation.add_error(field, "Expected Float got `{val.class_name}`")
510 return false
511 end
512 var min = self.min
513 if min != null and val < min then
514 v.validation.add_error(field, "Must be smaller or equal to `{min}`")
515 return false
516 end
517 var max = self.max
518 if max != null and val > max then
519 v.validation.add_error(field, "Must be greater or equal to `{max}`")
520 return false
521 end
522 return true
523 end
524 end
525
526 # Check that a field is a JsonObject
527 #
528 # ~~~
529 # var validator = new ObjectValidator
530 # validator.add new RequiredField("id", required = true)
531 # var user_val = new ObjectField("user")
532 # user_val.add new RequiredField("id", required = true)
533 # user_val.add new StringField("login", min_size=4)
534 # validator.add user_val
535 # assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
536 # assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
537 # ~~~
538 class ObjectField
539 super RequiredField
540 super ObjectValidator
541
542 redef var validation = new ValidationResult
543
544 redef fun validate_field(v, obj) do
545 if not super then return false
546 var val = obj.get_or_null(field)
547 if val == null then
548 if required == null or required == true then
549 v.validation.add_error(field, "Expected Object got `null`")
550 return false
551 else
552 return true
553 end
554 end
555 var res = validate_json(val)
556 for field, messages in validation.errors do
557 for message in messages do v.validation.add_error("{self.field}.{field}", message)
558 end
559 return res
560 end
561 end
562
563 # Check that a field is a JsonArray
564 #
565 # ~~~
566 # var validator = new ObjectValidator
567 # validator.add new RequiredField("id", required = true)
568 # validator.add new ArrayField("orders", allow_empty=false)
569 # assert not validator.validate("""{ "id": "", "orders": [] }""")
570 # assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
571 # ~~~
572 class ArrayField
573 super RequiredField
574 super ArrayValidator
575
576 autoinit field=, required=, allow_empty=, length=
577
578 redef var validation = new ValidationResult
579
580 redef fun validate_field(v, obj) do
581 if not super then return false
582 var val = obj.get_or_null(field)
583 if val == null then
584 if required == null or required == true then
585 v.validation.add_error(field, "Expected Array got `null`")
586 return false
587 else
588 return true
589 end
590 end
591 var res = validate_json(val)
592 for field, messages in validation.errors do
593 for message in messages do v.validation.add_error("{self.field}.{field}", message)
594 end
595 return res
596 end
597 end
598
599 # Check if two fields values match
600 #
601 # ~~~
602 # var validator = new ObjectValidator
603 # validator.add new FieldsMatch("field1", "field2")
604 #
605 # assert validator.validate("""{ "field1": {}, "field2": {} }""")
606 # assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
607 # assert validator.validate("""{ "field1": null, "field2": null }""")
608 # assert validator.validate("""{}""")
609 #
610 # assert not validator.validate("""{ "field1": {}, "field2": [] }""")
611 # assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
612 # assert not validator.validate("""{ "field1": null, "field2": "" }""")
613 # assert not validator.validate("""{ "field1": "foo" }""")
614 # ~~~
615 class FieldsMatch
616 super FieldValidator
617
618 # Other field to compare with
619 var other: String
620
621 redef fun validate_field(v, obj) do
622 var val1 = obj.get_or_null(field)
623 var val2 = obj.get_or_null(other)
624 if val1 != val2 then
625 v.validation.add_error(field, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
626 return false
627 end
628 return true
629 end
630 end
631
632 # Check if a field match a regular expression
633 #
634 # ~~~
635 # var validator = new ObjectValidator
636 # validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
637 # assert not validator.validate("""{ "title": "foo" }""")
638 # assert validator.validate("""{ "title": "Foo" }""")
639 # ~~~
640 class RegexField
641 super RequiredField
642
643 autoinit field, re, required
644
645 # Regular expression to match
646 var re: Regex
647
648 redef fun validate_field(v, obj) do
649 if not super then return false
650 var val = obj.get_or_null(field)
651 if val == null then
652 if required == null or required == true then
653 v.validation.add_error(field, "Expected String got `null`")
654 return false
655 else
656 return true
657 end
658 end
659 if not val isa String then
660 v.validation.add_error(field, "Expected String got `{val.class_name}`")
661 return false
662 end
663 if not val.has(re) then
664 v.validation.add_error(field, "Does not match `{re.string}`")
665 return false
666 end
667 return true
668 end
669 end
670
671 # Check if a field is a valid email
672 #
673 # ~~~
674 # var validator = new ObjectValidator
675 # validator.add new EmailField("email")
676 # assert not validator.validate("""{ "email": "" }""")
677 # assert not validator.validate("""{ "email": "foo" }""")
678 # assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
679 # assert validator.validate("""{ "email": "a+b@c.d" }""")
680 # ~~~
681 class EmailField
682 super RegexField
683
684 autoinit field, required
685
686 redef var re = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
687 end
688
689 # Check if a field is a valid ISBN
690 #
691 # ~~~
692 # var validator = new ObjectValidator
693 # validator.add new ISBNField("isbn")
694 # assert not validator.validate("""{ "isbn": "foo" }""")
695 # assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
696 # ~~~
697 class ISBNField
698 super RegexField
699
700 autoinit field, required
701
702 redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
703 end