1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 # Quick and easy validation framework for Json inputs
19 # Validators can be used in Popcorn apps to valid your json inputs before
20 # data processing and persistence.
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.
27 # import serialization
29 # # Serializable book representation.
39 # # Book image (optional)
40 # var img: nullable String
47 # super ObjectValidator
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)
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)
67 # # TODO data persistence
75 # The base class of all validators
76 abstract class DocumentValidator
80 # Accessible to the client after the `validate` method has been called.
81 var validation
: ValidationResult is noinit
83 # Validate the `document` input
85 # Result of the validation can be found in the `validation` attribute.
86 fun validate
(document
: String): Bool do
87 validation
= new ValidationResult
92 # Validation Result representation
94 # Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
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
101 # Object parsed during validation
103 # Can be used as a quick way to access the parsed JsonObject instead of
104 # reparsing it during the answer.
106 # See `ObjectValidator`.
107 var object
: nullable JsonObject = null is writable
109 # Array parsed during validation
111 # Can be used as a quick way to access the parsed JsonArray instead of
112 # reparsing it during the answer.
114 # See `ArrayValidator`.
115 var array
: nullable JsonArray = null is writable
117 # Errors found during validation
119 # Errors are grouped by scope.
120 var errors
= new HashMap[String, Array[String]]
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]
127 errors
[scope
].add message
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]
138 # Does `self` contains `errors`?
139 fun has_error
: Bool do return errors
.not_empty
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
)
146 v
.serialize_attribute
("has_error", has_error
)
147 v
.serialize_attribute
("errors", errors
)
150 # Returns the validation result as a pretty formated string
151 fun to_pretty_string
: String do
153 if not has_error
then
154 b
.append
"Everything is correct\n"
156 b
.append
"There is errors\n\n"
157 for k
, v
in errors
do
165 return b
.write_to_string
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 }""")
179 class ObjectValidator
180 super DocumentValidator
182 # Validators to apply on the object
183 var validators
= new Array[FieldValidator]
185 redef fun validate
(document
) do
187 var json
= document
.parse_json
189 validation
.add_error
("document", "Expected JsonObject got `null`")
192 return validate_json
(json
)
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}`")
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
210 fun add
(validator
: FieldValidator) do validators
.add validator
215 # var validator = new ArrayValidator
216 # assert not validator.validate("""{}""")
217 # assert validator.validate("""[]""")
218 # assert validator.validate("""[ "id", 10, {} ]""")
220 # validator = new ArrayValidator(allow_empty=false)
221 # assert not validator.validate("""[]""")
222 # assert validator.validate("""[ "id", 10, {} ]""")
224 # validator = new ArrayValidator(length=3)
225 # assert not validator.validate("""[]""")
226 # assert validator.validate("""[ "id", 10, {} ]""")
229 super DocumentValidator
231 # Allow empty arrays (default: true)
232 var allow_empty
: nullable Bool
234 # Check array length (default: no check)
235 var length
: nullable Int
237 redef fun validate
(document
) do
239 var json
= document
.parse_json
241 validation
.add_error
("document", "Expected JsonArray got `null`")
244 return validate_json
(json
)
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}`")
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")
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}`")
269 # Something that can validate a JsonObject field
270 abstract class FieldValidator
275 # Validate `field` in `obj`
276 fun validate_field
(v
: ObjectValidator, obj
: JsonObject): Bool is abstract
279 # Check if a field exists
282 # var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
283 # var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
284 # var json3 = """{ "field1": "", "field2": "foo" }"""
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)
292 # assert validator.validate(json1)
293 # assert validator.validate(json2)
294 # assert not validator.validate(json3)
295 # assert validator.validation.error("field3") == ["Required field"]
300 # Is this field required?
301 var required
: nullable Bool
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")
313 # Check if a field is a String
315 # `min_size` and `max_size` are optional
318 # var validator = new ObjectValidator
319 # validator.add new StringField("field", required=false)
320 # assert validator.validate("""{}""")
322 # validator = new ObjectValidator
323 # validator.add new StringField("field")
324 # assert not validator.validate("""{}""")
325 # assert not validator.validate("""{ "field": 10 }""")
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": "" }""")
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" }""")
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" }""")
348 # String min size (default: not checked)
349 var min_size
: nullable Int
351 # String max size (default: not checked)
352 var max_size
: nullable Int
354 redef fun validate_field
(v
, obj
) do
355 if not super then return false
356 var val
= obj
.get_or_null
(field
)
358 if required
== null or required
== true then
359 v
.validation
.add_error
(field
, "Expected String got `null`")
365 if not val
isa String then
366 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
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`")
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`")
383 # Check if a field is an Int
386 # var validator = new ObjectValidator
387 # validator.add new IntField("field", required=false)
388 # assert validator.validate("""{}""")
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 }""")
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 }""")
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 }""")
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 }""")
416 # Min value (default: not checked)
417 var min
: nullable Int
419 # Max value (default: not checked)
420 var max
: nullable Int
422 redef fun validate_field
(v
, obj
) do
423 if not super then return false
424 var val
= obj
.get_or_null
(field
)
426 if required
== null or required
== true then
427 v
.validation
.add_error
(field
, "Expected Int got `null`")
433 if not val
isa Int then
434 v
.validation
.add_error
(field
, "Expected Int got `{val.class_name}`")
438 if min
!= null and val
< min
then
439 v
.validation
.add_error
(field
, "Must be greater or equal to `{min}`")
443 if max
!= null and val
> max
then
444 v
.validation
.add_error
(field
, "Must be smaller or equal to `{max}`")
451 # Check if a field is a Float
454 # var validator = new ObjectValidator
455 # validator.add new FloatField("field", required=false)
456 # assert validator.validate("""{}""")
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 }""")
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 }""")
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 }""")
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 }""")
484 # Min value (default: not checked)
485 var min
: nullable Float
487 # Max value (default: not checked)
488 var max
: nullable Float
490 redef fun validate_field
(v
, obj
) do
491 if not super then return false
492 var val
= obj
.get_or_null
(field
)
494 if required
== null or required
== true then
495 v
.validation
.add_error
(field
, "Expected Float got `null`")
501 if not val
isa Float then
502 v
.validation
.add_error
(field
, "Expected Float got `{val.class_name}`")
506 if min
!= null and val
< min
then
507 v
.validation
.add_error
(field
, "Must be smaller or equal to `{min}`")
511 if max
!= null and val
> max
then
512 v
.validation
.add_error
(field
, "Must be greater or equal to `{max}`")
519 # Check if a field is a Bool
522 # var validator = new ObjectValidator
523 # validator.add new BoolField("field", required=false)
524 # assert validator.validate("""{}""")
525 # assert validator.validate("""{ "field": true }""")
526 # assert validator.validate("""{ "field": false }""")
527 # assert not validator.validate("""{ "field": "foo" }""")
529 # validator = new ObjectValidator
530 # validator.add new BoolField("field")
531 # assert not validator.validate("""{}""")
532 # assert validator.validate("""{ "field": true }""")
533 # assert validator.validate("""{ "field": false }""")
534 # assert not validator.validate("""{ "field": "foo" }""")
537 # No type conversion is applied on the input value:
539 # assert not validator.validate("""{ "field": "true" }""")
540 # assert not validator.validate("""{ "field": 1 }""")
541 # assert not validator.validate("""{ "field": [true] }""")
546 redef fun validate_field
(v
, obj
) do
547 if not super then return false
548 var val
= obj
.get_or_null
(field
)
550 if required
== null or required
== true then
551 v
.validation
.add_error
(field
, "Expected Bool got `null`")
557 if not val
isa Bool then
558 v
.validation
.add_error
(field
, "Expected Bool got `{val.class_name}`")
565 # Check that a field is a JsonObject
568 # var validator = new ObjectValidator
569 # validator.add new RequiredField("id", required = true)
570 # var user_val = new ObjectField("user")
571 # user_val.add new RequiredField("id", required = true)
572 # user_val.add new StringField("login", min_size=4)
573 # validator.add user_val
574 # assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
575 # assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
579 super ObjectValidator
581 redef var validation
= new ValidationResult
583 redef fun validate_field
(v
, obj
) do
584 if not super then return false
585 var val
= obj
.get_or_null
(field
)
587 if required
== null or required
== true then
588 v
.validation
.add_error
(field
, "Expected Object got `null`")
594 var res
= validate_json
(val
)
595 for field
, messages
in validation
.errors
do
596 for message
in messages
do v
.validation
.add_error
("{self.field}.{field}", message
)
602 # Check that a field is a JsonArray
605 # var validator = new ObjectValidator
606 # validator.add new RequiredField("id", required = true)
607 # validator.add new ArrayField("orders", allow_empty=false)
608 # assert not validator.validate("""{ "id": "", "orders": [] }""")
609 # assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
615 autoinit field
=, required
=, allow_empty
=, length
=
617 redef var validation
= new ValidationResult
619 redef fun validate_field
(v
, obj
) do
620 if not super then return false
621 var val
= obj
.get_or_null
(field
)
623 if required
== null or required
== true then
624 v
.validation
.add_error
(field
, "Expected Array got `null`")
630 var res
= validate_json
(val
)
631 for field
, messages
in validation
.errors
do
632 for message
in messages
do v
.validation
.add_error
("{self.field}.{field}", message
)
638 # Check if two fields values match
641 # var validator = new ObjectValidator
642 # validator.add new FieldsMatch("field1", "field2")
644 # assert validator.validate("""{ "field1": {}, "field2": {} }""")
645 # assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
646 # assert validator.validate("""{ "field1": null, "field2": null }""")
647 # assert validator.validate("""{}""")
649 # assert not validator.validate("""{ "field1": {}, "field2": [] }""")
650 # assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
651 # assert not validator.validate("""{ "field1": null, "field2": "" }""")
652 # assert not validator.validate("""{ "field1": "foo" }""")
657 # Other field to compare with
660 redef fun validate_field
(v
, obj
) do
661 var val1
= obj
.get_or_null
(field
)
662 var val2
= obj
.get_or_null
(other
)
664 v
.validation
.add_error
(field
, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
671 # Check if a field match a regular expression
674 # var validator = new ObjectValidator
675 # validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
676 # assert not validator.validate("""{ "title": "foo" }""")
677 # assert validator.validate("""{ "title": "Foo" }""")
682 autoinit field
, re
, required
684 # Regular expression to match
687 redef fun validate_field
(v
, obj
) do
688 if not super then return false
689 var val
= obj
.get_or_null
(field
)
691 if required
== null or required
== true then
692 v
.validation
.add_error
(field
, "Expected String got `null`")
698 if not val
isa String then
699 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
702 if not val
.has
(re
) then
703 v
.validation
.add_error
(field
, "Does not match `{re.string}`")
710 # Check if a field is a valid email
713 # var validator = new ObjectValidator
714 # validator.add new EmailField("email")
715 # assert not validator.validate("""{ "email": "" }""")
716 # assert not validator.validate("""{ "email": "foo" }""")
717 # assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
718 # assert validator.validate("""{ "email": "a+b@c.d" }""")
723 autoinit field
, required
725 redef var re
= "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
728 # Check if a field is a valid ISBN
731 # var validator = new ObjectValidator
732 # validator.add new ISBNField("isbn")
733 # assert not validator.validate("""{ "isbn": "foo" }""")
734 # assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
739 autoinit field
, required
741 redef var re
= "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
744 # Check if a field is a valid URL
746 # Matched against the following regular expression:
748 # ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&:/~\+#]*[a-zA-Z0-9\-\@?^=%&/~\+#])?
750 # You should redefine the base regular expression `re` with your own.
753 # var validator = new ObjectValidator
754 # validator.add new URLField("url")
755 # assert not validator.validate("""{ "url": "" }""")
756 # assert not validator.validate("""{ "url": "foo" }""")
757 # assert not validator.validate("""{ "url": "http://foo" }""")
758 # assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
759 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
760 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
761 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
762 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
767 autoinit field
, required
769 redef var re
= "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
772 # Check if a field value is already used
774 # This class provides a stub validator for fields that should contain a unique value along an
775 # application (typically logins or ids).
777 # Here an example that uses a `Repository` if an email is unique:
779 # class UniqueEmailField
782 # var users: UsersRepository
784 # redef fun check_unicity(v, field, val) do
785 # var user = users.find_by_email(val)
786 # if user != null then
787 # v.validation.add_error(field, "Email `{val}` already used")
797 # Check if `val` is already used somewhere
799 # You must redefine this method to handle your own validation.
800 fun check_unicity
(v
: ObjectValidator, field
, val
: String): Bool is abstract
802 redef fun validate_field
(v
, obj
) do
803 if not super then return false
804 var val
= obj
.get_or_null
(field
)
805 if not val
isa String then return false
806 if not check_unicity
(v
, field
, val
) then return false