gamnit: add server discovery or create example
[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 Serializable
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(validator.validation, 400)
65 # return
66 # end
67 # # TODO data persistence
68 # end
69 # end
70 # ~~~
71 module pop_validation
72
73 import json::static
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 class ValidationResult
99 super Serializable
100
101 # Object parsed during validation
102 #
103 # Can be used as a quick way to access the parsed JsonObject instead of
104 # reparsing it during the answer.
105 #
106 # See `ObjectValidator`.
107 var object: nullable JsonObject = null is writable
108
109 # Array parsed during validation
110 #
111 # Can be used as a quick way to access the parsed JsonArray instead of
112 # reparsing it during the answer.
113 #
114 # See `ArrayValidator`.
115 var array: nullable JsonArray = null is writable
116
117 # Errors found during validation
118 #
119 # Errors are grouped by scope.
120 var errors = new HashMap[String, Array[String]]
121
122 # Generate a new error `message` into `scope`
123 fun add_error(scope, message: String) do
124 if not errors.has_key(scope) then
125 errors[scope] = new Array[String]
126 end
127 errors[scope].add message
128 end
129
130 # Get the errors for `scope`
131 fun error(scope: String): Array[String] do
132 if not errors.has_key(scope) then
133 return new Array[String]
134 end
135 return errors[scope]
136 end
137
138 # Does `self` contains `errors`?
139 fun has_error: Bool do return errors.not_empty
140
141 redef fun core_serialize_to(v) do
142 var errors = new JsonObject
143 for k, e in self.errors do
144 errors[k] = new JsonArray.from(e)
145 end
146 v.serialize_attribute("has_error", has_error)
147 v.serialize_attribute("errors", errors)
148 end
149
150 # Returns the validation result as a pretty formated string
151 fun to_pretty_string: String do
152 var b = new Buffer
153 if not has_error then
154 b.append "Everything is correct\n"
155 else
156 b.append "There is errors\n\n"
157 for k, v in errors do
158 b.append "{k}:\n"
159 for vv in v do
160 b.append "\t{vv}\n"
161 end
162 b.append "\n"
163 end
164 end
165 return b.write_to_string
166 end
167 end
168
169 # Check a JsonObject
170 # ~~~
171 # var validator = new ObjectValidator
172 # validator.add new RequiredField("id", required = true)
173 # validator.add new StringField("login", min_size=4)
174 # validator.add new IntField("age", min=0, max=100)
175 # assert not validator.validate("""{}""")
176 # assert not validator.validate("""[]""")
177 # assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
178 # ~~~
179 class ObjectValidator
180 super DocumentValidator
181
182 # Validators to apply on the object
183 var validators = new Array[FieldValidator]
184
185 redef fun validate(document) do
186 super
187 var json = document.parse_json
188 if json == null then
189 validation.add_error("document", "Expected JsonObject got `null`")
190 return false
191 end
192 return validate_json(json)
193 end
194
195 # Validate a Serializable input
196 fun validate_json(json: Serializable): Bool do
197 if not json isa JsonObject then
198 validation.add_error("document", "Expected JsonObject got `{json.class_name}`")
199 return false
200 end
201 validation.object = json
202 for validator in validators do
203 var res = validator.validate_field(self, json)
204 if not res then return false
205 end
206 return true
207 end
208
209 # Add a validator
210 fun add(validator: FieldValidator) do validators.add validator
211 end
212
213 # Check a JsonArray
214 # ~~~
215 # var validator = new ArrayValidator
216 # assert not validator.validate("""{}""")
217 # assert validator.validate("""[]""")
218 # assert validator.validate("""[ "id", 10, {} ]""")
219 #
220 # validator = new ArrayValidator(allow_empty=false)
221 # assert not validator.validate("""[]""")
222 # assert validator.validate("""[ "id", 10, {} ]""")
223 #
224 # validator = new ArrayValidator(length=3)
225 # assert not validator.validate("""[]""")
226 # assert validator.validate("""[ "id", 10, {} ]""")
227 # ~~~
228 class ArrayValidator
229 super DocumentValidator
230
231 # Allow empty arrays (default: true)
232 var allow_empty: nullable Bool
233
234 # Check array length (default: no check)
235 var length: nullable Int
236
237 redef fun validate(document) do
238 super
239 var json = document.parse_json
240 if json == null then
241 validation.add_error("document", "Expected JsonArray got `null`")
242 return false
243 end
244 return validate_json(json)
245 end
246
247 # Validate a Serializable input
248 fun validate_json(json: Serializable): Bool do
249 if not json isa JsonArray then
250 validation.add_error("document", "Expected JsonArray got `{json.class_name}`")
251 return false
252 end
253 validation.array = json
254 var allow_empty = self.allow_empty
255 if json.is_empty and (allow_empty != null and not allow_empty) then
256 validation.add_error("document", "Cannot be empty")
257 return false
258 end
259 var length = self.length
260 if length != null and json.length != length then
261 validation.add_error("document", "Array length must be exactly `{length}`")
262 return false
263 end
264
265 return true
266 end
267 end
268
269 # Something that can validate a JsonObject field
270 abstract class FieldValidator
271
272 # Field to validate
273 var field: String
274
275 # Validate `field` in `obj`
276 fun validate_field(v: ObjectValidator, obj: JsonObject): Bool is abstract
277 end
278
279 # Check if a field exists
280 #
281 # ~~~
282 # var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
283 # var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
284 # var json3 = """{ "field1": "", "field2": "foo" }"""
285 #
286 # var validator = new ObjectValidator
287 # validator.add new RequiredField("field1")
288 # validator.add new RequiredField("field2")
289 # validator.add new RequiredField("field3")
290 # validator.add new RequiredField("field4", required=false)
291 #
292 # assert validator.validate(json1)
293 # assert validator.validate(json2)
294 # assert not validator.validate(json3)
295 # assert validator.validation.error("field3") == ["Required field"]
296 # ~~~
297 class RequiredField
298 super FieldValidator
299
300 # Is this field required?
301 var required: nullable Bool
302
303 redef fun validate_field(v, obj) do
304 var required = self.required
305 if (required != null and required or required == null) and not obj.has_key(field) then
306 v.validation.add_error(field, "Required field")
307 return false
308 end
309 return true
310 end
311 end
312
313 # Check if a field is a String
314 #
315 # `min_size` and `max_size` are optional
316 #
317 # ~~~
318 # var validator = new ObjectValidator
319 # validator.add new StringField("field", required=false)
320 # assert validator.validate("""{}""")
321 #
322 # validator = new ObjectValidator
323 # validator.add new StringField("field")
324 # assert not validator.validate("""{}""")
325 # assert not validator.validate("""{ "field": 10 }""")
326 #
327 # validator = new ObjectValidator
328 # validator.add new StringField("field", min_size=3)
329 # assert validator.validate("""{ "field": "foo" }""")
330 # assert not validator.validate("""{ "field": "fo" }""")
331 # assert not validator.validate("""{ "field": "" }""")
332 #
333 # validator = new ObjectValidator
334 # validator.add new StringField("field", max_size=3)
335 # assert validator.validate("""{ "field": "foo" }""")
336 # assert not validator.validate("""{ "field": "fooo" }""")
337 #
338 # validator = new ObjectValidator
339 # validator.add new StringField("field", min_size=3, max_size=5)
340 # assert not validator.validate("""{ "field": "fo" }""")
341 # assert validator.validate("""{ "field": "foo" }""")
342 # assert validator.validate("""{ "field": "foooo" }""")
343 # assert not validator.validate("""{ "field": "fooooo" }""")
344 # ~~~
345 class StringField
346 super RequiredField
347
348 # String min size (default: not checked)
349 var min_size: nullable Int
350
351 # String max size (default: not checked)
352 var max_size: nullable Int
353
354 redef fun validate_field(v, obj) do
355 if not super then return false
356 var val = obj.get_or_null(field)
357 if val == null then
358 if required == null or required == true then
359 v.validation.add_error(field, "Expected String got `null`")
360 return false
361 else
362 return true
363 end
364 end
365 if not val isa String then
366 v.validation.add_error(field, "Expected String got `{val.class_name}`")
367 return false
368 end
369 var min_size = self.min_size
370 if min_size != null and val.length < min_size then
371 v.validation.add_error(field, "Must be at least `{min_size} characters long`")
372 return false
373 end
374 var max_size = self.max_size
375 if max_size != null and val.length > max_size then
376 v.validation.add_error(field, "Must be at max `{max_size} characters long`")
377 return false
378 end
379 return true
380 end
381 end
382
383 # Check if a field is an Int
384 #
385 # ~~~
386 # var validator = new ObjectValidator
387 # validator.add new IntField("field", required=false)
388 # assert validator.validate("""{}""")
389 #
390 # validator = new ObjectValidator
391 # validator.add new IntField("field")
392 # assert not validator.validate("""{}""")
393 # assert not validator.validate("""{ "field": "foo" }""")
394 # assert validator.validate("""{ "field": 10 }""")
395 #
396 # validator = new ObjectValidator
397 # validator.add new IntField("field", min=3)
398 # assert validator.validate("""{ "field": 3 }""")
399 # assert not validator.validate("""{ "field": 2 }""")
400 #
401 # validator = new ObjectValidator
402 # validator.add new IntField("field", max=3)
403 # assert validator.validate("""{ "field": 3 }""")
404 # assert not validator.validate("""{ "field": 4 }""")
405 #
406 # validator = new ObjectValidator
407 # validator.add new IntField("field", min=3, max=5)
408 # assert not validator.validate("""{ "field": 2 }""")
409 # assert validator.validate("""{ "field": 3 }""")
410 # assert validator.validate("""{ "field": 5 }""")
411 # assert not validator.validate("""{ "field": 6 }""")
412 # ~~~
413 class IntField
414 super RequiredField
415
416 # Min value (default: not checked)
417 var min: nullable Int
418
419 # Max value (default: not checked)
420 var max: nullable Int
421
422 redef fun validate_field(v, obj) do
423 if not super then return false
424 var val = obj.get_or_null(field)
425 if val == null then
426 if required == null or required == true then
427 v.validation.add_error(field, "Expected Int got `null`")
428 return false
429 else
430 return true
431 end
432 end
433 if not val isa Int then
434 v.validation.add_error(field, "Expected Int got `{val.class_name}`")
435 return false
436 end
437 var min = self.min
438 if min != null and val < min then
439 v.validation.add_error(field, "Must be greater or equal to `{min}`")
440 return false
441 end
442 var max = self.max
443 if max != null and val > max then
444 v.validation.add_error(field, "Must be smaller or equal to `{max}`")
445 return false
446 end
447 return true
448 end
449 end
450
451 # Check if a field is a Float
452 #
453 # ~~~
454 # var validator = new ObjectValidator
455 # validator.add new FloatField("field", required=false)
456 # assert validator.validate("""{}""")
457 #
458 # validator = new ObjectValidator
459 # validator.add new FloatField("field")
460 # assert not validator.validate("""{}""")
461 # assert not validator.validate("""{ "field": "foo" }""")
462 # assert validator.validate("""{ "field": 10.5 }""")
463 #
464 # validator = new ObjectValidator
465 # validator.add new FloatField("field", min=3.0)
466 # assert validator.validate("""{ "field": 3.0 }""")
467 # assert not validator.validate("""{ "field": 2.0 }""")
468 #
469 # validator = new ObjectValidator
470 # validator.add new FloatField("field", max=3.0)
471 # assert validator.validate("""{ "field": 3.0 }""")
472 # assert not validator.validate("""{ "field": 4.0 }""")
473 #
474 # validator = new ObjectValidator
475 # validator.add new FloatField("field", min=3.0, max=5.0)
476 # assert not validator.validate("""{ "field": 2.0 }""")
477 # assert validator.validate("""{ "field": 3.0 }""")
478 # assert validator.validate("""{ "field": 5.0 }""")
479 # assert not validator.validate("""{ "field": 6.0 }""")
480 # ~~~
481 class FloatField
482 super RequiredField
483
484 # Min value (default: not checked)
485 var min: nullable Float
486
487 # Max value (default: not checked)
488 var max: nullable Float
489
490 redef fun validate_field(v, obj) do
491 if not super then return false
492 var val = obj.get_or_null(field)
493 if val == null then
494 if required == null or required == true then
495 v.validation.add_error(field, "Expected Float got `null`")
496 return false
497 else
498 return true
499 end
500 end
501 if not val isa Float then
502 v.validation.add_error(field, "Expected Float got `{val.class_name}`")
503 return false
504 end
505 var min = self.min
506 if min != null and val < min then
507 v.validation.add_error(field, "Must be smaller or equal to `{min}`")
508 return false
509 end
510 var max = self.max
511 if max != null and val > max then
512 v.validation.add_error(field, "Must be greater or equal to `{max}`")
513 return false
514 end
515 return true
516 end
517 end
518
519 # Check that a field is a JsonObject
520 #
521 # ~~~
522 # var validator = new ObjectValidator
523 # validator.add new RequiredField("id", required = true)
524 # var user_val = new ObjectField("user")
525 # user_val.add new RequiredField("id", required = true)
526 # user_val.add new StringField("login", min_size=4)
527 # validator.add user_val
528 # assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
529 # assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
530 # ~~~
531 class ObjectField
532 super RequiredField
533 super ObjectValidator
534
535 redef var validation = new ValidationResult
536
537 redef fun validate_field(v, obj) do
538 if not super then return false
539 var val = obj.get_or_null(field)
540 if val == null then
541 if required == null or required == true then
542 v.validation.add_error(field, "Expected Object got `null`")
543 return false
544 else
545 return true
546 end
547 end
548 var res = validate_json(val)
549 for field, messages in validation.errors do
550 for message in messages do v.validation.add_error("{self.field}.{field}", message)
551 end
552 return res
553 end
554 end
555
556 # Check that a field is a JsonArray
557 #
558 # ~~~
559 # var validator = new ObjectValidator
560 # validator.add new RequiredField("id", required = true)
561 # validator.add new ArrayField("orders", allow_empty=false)
562 # assert not validator.validate("""{ "id": "", "orders": [] }""")
563 # assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
564 # ~~~
565 class ArrayField
566 super RequiredField
567 super ArrayValidator
568
569 autoinit field=, required=, allow_empty=, length=
570
571 redef var validation = new ValidationResult
572
573 redef fun validate_field(v, obj) do
574 if not super then return false
575 var val = obj.get_or_null(field)
576 if val == null then
577 if required == null or required == true then
578 v.validation.add_error(field, "Expected Array got `null`")
579 return false
580 else
581 return true
582 end
583 end
584 var res = validate_json(val)
585 for field, messages in validation.errors do
586 for message in messages do v.validation.add_error("{self.field}.{field}", message)
587 end
588 return res
589 end
590 end
591
592 # Check if two fields values match
593 #
594 # ~~~
595 # var validator = new ObjectValidator
596 # validator.add new FieldsMatch("field1", "field2")
597 #
598 # assert validator.validate("""{ "field1": {}, "field2": {} }""")
599 # assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
600 # assert validator.validate("""{ "field1": null, "field2": null }""")
601 # assert validator.validate("""{}""")
602 #
603 # assert not validator.validate("""{ "field1": {}, "field2": [] }""")
604 # assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
605 # assert not validator.validate("""{ "field1": null, "field2": "" }""")
606 # assert not validator.validate("""{ "field1": "foo" }""")
607 # ~~~
608 class FieldsMatch
609 super FieldValidator
610
611 # Other field to compare with
612 var other: String
613
614 redef fun validate_field(v, obj) do
615 var val1 = obj.get_or_null(field)
616 var val2 = obj.get_or_null(other)
617 if val1 != val2 then
618 v.validation.add_error(field, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
619 return false
620 end
621 return true
622 end
623 end
624
625 # Check if a field match a regular expression
626 #
627 # ~~~
628 # var validator = new ObjectValidator
629 # validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
630 # assert not validator.validate("""{ "title": "foo" }""")
631 # assert validator.validate("""{ "title": "Foo" }""")
632 # ~~~
633 class RegexField
634 super RequiredField
635
636 autoinit field, re, required
637
638 # Regular expression to match
639 var re: Regex
640
641 redef fun validate_field(v, obj) do
642 if not super then return false
643 var val = obj.get_or_null(field)
644 if val == null then
645 if required == null or required == true then
646 v.validation.add_error(field, "Expected String got `null`")
647 return false
648 else
649 return true
650 end
651 end
652 if not val isa String then
653 v.validation.add_error(field, "Expected String got `{val.class_name}`")
654 return false
655 end
656 if not val.has(re) then
657 v.validation.add_error(field, "Does not match `{re.string}`")
658 return false
659 end
660 return true
661 end
662 end
663
664 # Check if a field is a valid email
665 #
666 # ~~~
667 # var validator = new ObjectValidator
668 # validator.add new EmailField("email")
669 # assert not validator.validate("""{ "email": "" }""")
670 # assert not validator.validate("""{ "email": "foo" }""")
671 # assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
672 # assert validator.validate("""{ "email": "a+b@c.d" }""")
673 # ~~~
674 class EmailField
675 super RegexField
676
677 autoinit field, required
678
679 redef var re = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
680 end
681
682 # Check if a field is a valid ISBN
683 #
684 # ~~~
685 # var validator = new ObjectValidator
686 # validator.add new ISBNField("isbn")
687 # assert not validator.validate("""{ "isbn": "foo" }""")
688 # assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
689 # ~~~
690 class ISBNField
691 super RegexField
692
693 autoinit field, required
694
695 redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
696 end
697
698 # Check if a field is a valid URL
699 #
700 # Matched against the following regular expression:
701 # ~~~raw
702 # ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&amp;:/~\+#]*[a-zA-Z0-9\-\@?^=%&amp;/~\+#])?
703 # ~~~
704 # You should redefine the base regular expression `re` with your own.
705 #
706 # ~~~
707 # var validator = new ObjectValidator
708 # validator.add new URLField("url")
709 # assert not validator.validate("""{ "url": "" }""")
710 # assert not validator.validate("""{ "url": "foo" }""")
711 # assert not validator.validate("""{ "url": "http://foo" }""")
712 # assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
713 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
714 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
715 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
716 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
717 # ~~~
718 class URLField
719 super RegexField
720
721 autoinit field, required
722
723 redef var re = "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
724 end
725
726 # Check if a field value is already used
727 #
728 # This class provides a stub validator for fields that should contain a unique value along an
729 # application (typically logins or ids).
730 #
731 # Here an example that uses a `Repository` if an email is unique:
732 # ~~~nitish
733 # class UniqueEmailField
734 # super UniqueField
735 #
736 # var users: UsersRepository
737 #
738 # redef fun check_unicity(v, field, val) do
739 # var user = users.find_by_email(val)
740 # if user != null then
741 # v.validation.add_error(field, "Email `{val}` already used")
742 # return false
743 # end
744 # return true
745 # end
746 # end
747 # ~~~
748 class UniqueField
749 super StringField
750
751 # Check if `val` is already used somewhere
752 #
753 # You must redefine this method to handle your own validation.
754 fun check_unicity(v: ObjectValidator, field, val: String): Bool is abstract
755
756 redef fun validate_field(v, obj) do
757 if not super then return false
758 var val = obj.get_or_null(field)
759 if not val isa String then return false
760 if not check_unicity(v, field, val) then return false
761 return true
762 end
763 end