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 popcorn::pop_json
28 # import serialization
30 # # Serializable book representation.
40 # # Book image (optional)
41 # var img: nullable String
48 # super ObjectValidator
51 # add new ISBNField("isbn")
52 # add new StringField("title", min_size=1, max_size=255)
53 # add new StringField("img", required=false)
54 # add new FloatField("price", min=0.0, max=999.0)
62 # redef fun post(req, res) do
63 # var validator = new BookValidator
64 # if not validator.validate(req.body) then
65 # res.json(validator.validation, 400)
68 # # TODO data persistence
76 # The base class of all validators
77 abstract class DocumentValidator
81 # Accessible to the client after the `validate` method has been called.
82 var validation
: ValidationResult is noinit
84 # Validate the `document` input
86 # Result of the validation can be found in the `validation` attribute.
87 fun validate
(document
: String): Bool do
88 validation
= new ValidationResult
93 # Validation Result representation
95 # Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
97 # Errors messages are grouped into *scopes*. A scope is a string that specify wich
98 # field or document the error message is related to.
99 class ValidationResult
102 # Object parsed during validation
104 # Can be used as a quick way to access the parsed JsonObject instead of
105 # reparsing it during the answer.
107 # See `ObjectValidator`.
108 var object
: nullable JsonObject = null is writable
110 # Array parsed during validation
112 # Can be used as a quick way to access the parsed JsonArray instead of
113 # reparsing it during the answer.
115 # See `ArrayValidator`.
116 var array
: nullable JsonArray = null is writable
118 # Errors found during validation
120 # Errors are grouped by scope.
121 var errors
= new HashMap[String, Array[String]]
123 # Generate a new error `message` into `scope`
124 fun add_error
(scope
, message
: String) do
125 if not errors
.has_key
(scope
) then
126 errors
[scope
] = new Array[String]
128 errors
[scope
].add message
131 # Get the errors for `scope`
132 fun error
(scope
: String): Array[String] do
133 if not errors
.has_key
(scope
) then
134 return new Array[String]
139 # Does `self` contains `errors`?
140 fun has_error
: Bool do return errors
.not_empty
142 redef fun core_serialize_to
(v
) do
143 var errors
= new JsonObject
144 for k
, e
in self.errors
do
145 errors
[k
] = new JsonArray.from
(e
)
147 v
.serialize_attribute
("has_error", has_error
)
148 v
.serialize_attribute
("errors", errors
)
151 # Returns the validation result as a pretty formated string
152 fun to_pretty_string
: String do
154 if not has_error
then
155 b
.append
"Everything is correct\n"
157 b
.append
"There is errors\n\n"
158 for k
, v
in errors
do
166 return b
.write_to_string
172 # var validator = new ObjectValidator
173 # validator.add new RequiredField("id", required = true)
174 # validator.add new StringField("login", min_size=4)
175 # validator.add new IntField("age", min=0, max=100)
176 # assert not validator.validate("""{}""")
177 # assert not validator.validate("""[]""")
178 # assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
180 class ObjectValidator
181 super DocumentValidator
183 # Validators to apply on the object
184 var validators
= new Array[FieldValidator]
186 redef fun validate
(document
) do
188 var json
= document
.parse_json
190 validation
.add_error
("document", "Expected JsonObject got `null`")
193 return validate_json
(json
)
196 # Validate a Serializable input
197 fun validate_json
(json
: Serializable): Bool do
198 if not json
isa JsonObject then
199 validation
.add_error
("document", "Expected JsonObject got `{json.class_name}`")
202 validation
.object
= json
203 for validator
in validators
do
204 var res
= validator
.validate_field
(self, json
)
205 if not res
then return false
211 fun add
(validator
: FieldValidator) do validators
.add validator
216 # var validator = new ArrayValidator
217 # assert not validator.validate("""{}""")
218 # assert validator.validate("""[]""")
219 # assert validator.validate("""[ "id", 10, {} ]""")
221 # validator = new ArrayValidator(allow_empty=false)
222 # assert not validator.validate("""[]""")
223 # assert validator.validate("""[ "id", 10, {} ]""")
225 # validator = new ArrayValidator(length=3)
226 # assert not validator.validate("""[]""")
227 # assert validator.validate("""[ "id", 10, {} ]""")
230 super DocumentValidator
232 # Allow empty arrays (default: true)
233 var allow_empty
: nullable Bool
235 # Check array length (default: no check)
236 var length
: nullable Int
238 redef fun validate
(document
) do
240 var json
= document
.parse_json
242 validation
.add_error
("document", "Expected JsonArray got `null`")
245 return validate_json
(json
)
248 # Validate a Serializable input
249 fun validate_json
(json
: Serializable): Bool do
250 if not json
isa JsonArray then
251 validation
.add_error
("document", "Expected JsonArray got `{json.class_name}`")
254 validation
.array
= json
255 var allow_empty
= self.allow_empty
256 if json
.is_empty
and (allow_empty
!= null and not allow_empty
) then
257 validation
.add_error
("document", "Cannot be empty")
260 var length
= self.length
261 if length
!= null and json
.length
!= length
then
262 validation
.add_error
("document", "Array length must be exactly `{length}`")
270 # Something that can validate a JsonObject field
271 abstract class FieldValidator
276 # Validate `field` in `obj`
277 fun validate_field
(v
: ObjectValidator, obj
: JsonObject): Bool is abstract
280 # Check if a field exists
283 # var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
284 # var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
285 # var json3 = """{ "field1": "", "field2": "foo" }"""
287 # var validator = new ObjectValidator
288 # validator.add new RequiredField("field1")
289 # validator.add new RequiredField("field2")
290 # validator.add new RequiredField("field3")
291 # validator.add new RequiredField("field4", required=false)
293 # assert validator.validate(json1)
294 # assert validator.validate(json2)
295 # assert not validator.validate(json3)
296 # assert validator.validation.error("field3") == ["Required field"]
301 # Is this field required?
302 var required
: nullable Bool
304 redef fun validate_field
(v
, obj
) do
305 var required
= self.required
306 if (required
!= null and required
or required
== null) and not obj
.has_key
(field
) then
307 v
.validation
.add_error
(field
, "Required field")
314 # Check if a field is a String
316 # `min_size` and `max_size` are optional
319 # var validator = new ObjectValidator
320 # validator.add new StringField("field", required=false)
321 # assert validator.validate("""{}""")
323 # validator = new ObjectValidator
324 # validator.add new StringField("field")
325 # assert not validator.validate("""{}""")
326 # assert not validator.validate("""{ "field": 10 }""")
328 # validator = new ObjectValidator
329 # validator.add new StringField("field", min_size=3)
330 # assert validator.validate("""{ "field": "foo" }""")
331 # assert not validator.validate("""{ "field": "fo" }""")
332 # assert not validator.validate("""{ "field": "" }""")
334 # validator = new ObjectValidator
335 # validator.add new StringField("field", max_size=3)
336 # assert validator.validate("""{ "field": "foo" }""")
337 # assert not validator.validate("""{ "field": "fooo" }""")
339 # validator = new ObjectValidator
340 # validator.add new StringField("field", min_size=3, max_size=5)
341 # assert not validator.validate("""{ "field": "fo" }""")
342 # assert validator.validate("""{ "field": "foo" }""")
343 # assert validator.validate("""{ "field": "foooo" }""")
344 # assert not validator.validate("""{ "field": "fooooo" }""")
349 # String min size (default: not checked)
350 var min_size
: nullable Int
352 # String max size (default: not checked)
353 var max_size
: nullable Int
355 redef fun validate_field
(v
, obj
) do
356 if not super then return false
357 var val
= obj
.get_or_null
(field
)
359 if required
== null or required
== true then
360 v
.validation
.add_error
(field
, "Expected String got `null`")
366 if not val
isa String then
367 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
370 var min_size
= self.min_size
371 if min_size
!= null and val
.length
< min_size
then
372 v
.validation
.add_error
(field
, "Must be at least `{min_size} characters long`")
375 var max_size
= self.max_size
376 if max_size
!= null and val
.length
> max_size
then
377 v
.validation
.add_error
(field
, "Must be at max `{max_size} characters long`")
384 # Check if a field is an Int
387 # var validator = new ObjectValidator
388 # validator.add new IntField("field", required=false)
389 # assert validator.validate("""{}""")
391 # validator = new ObjectValidator
392 # validator.add new IntField("field")
393 # assert not validator.validate("""{}""")
394 # assert not validator.validate("""{ "field": "foo" }""")
395 # assert validator.validate("""{ "field": 10 }""")
397 # validator = new ObjectValidator
398 # validator.add new IntField("field", min=3)
399 # assert validator.validate("""{ "field": 3 }""")
400 # assert not validator.validate("""{ "field": 2 }""")
402 # validator = new ObjectValidator
403 # validator.add new IntField("field", max=3)
404 # assert validator.validate("""{ "field": 3 }""")
405 # assert not validator.validate("""{ "field": 4 }""")
407 # validator = new ObjectValidator
408 # validator.add new IntField("field", min=3, max=5)
409 # assert not validator.validate("""{ "field": 2 }""")
410 # assert validator.validate("""{ "field": 3 }""")
411 # assert validator.validate("""{ "field": 5 }""")
412 # assert not validator.validate("""{ "field": 6 }""")
417 # Min value (default: not checked)
418 var min
: nullable Int
420 # Max value (default: not checked)
421 var max
: nullable Int
423 redef fun validate_field
(v
, obj
) do
424 if not super then return false
425 var val
= obj
.get_or_null
(field
)
427 if required
== null or required
== true then
428 v
.validation
.add_error
(field
, "Expected Int got `null`")
434 if not val
isa Int then
435 v
.validation
.add_error
(field
, "Expected Int got `{val.class_name}`")
439 if min
!= null and val
< min
then
440 v
.validation
.add_error
(field
, "Must be greater or equal to `{min}`")
444 if max
!= null and val
> max
then
445 v
.validation
.add_error
(field
, "Must be smaller or equal to `{max}`")
452 # Check if a field is a Float
455 # var validator = new ObjectValidator
456 # validator.add new FloatField("field", required=false)
457 # assert validator.validate("""{}""")
459 # validator = new ObjectValidator
460 # validator.add new FloatField("field")
461 # assert not validator.validate("""{}""")
462 # assert not validator.validate("""{ "field": "foo" }""")
463 # assert validator.validate("""{ "field": 10.5 }""")
465 # validator = new ObjectValidator
466 # validator.add new FloatField("field", min=3.0)
467 # assert validator.validate("""{ "field": 3.0 }""")
468 # assert not validator.validate("""{ "field": 2.0 }""")
470 # validator = new ObjectValidator
471 # validator.add new FloatField("field", max=3.0)
472 # assert validator.validate("""{ "field": 3.0 }""")
473 # assert not validator.validate("""{ "field": 4.0 }""")
475 # validator = new ObjectValidator
476 # validator.add new FloatField("field", min=3.0, max=5.0)
477 # assert not validator.validate("""{ "field": 2.0 }""")
478 # assert validator.validate("""{ "field": 3.0 }""")
479 # assert validator.validate("""{ "field": 5.0 }""")
480 # assert not validator.validate("""{ "field": 6.0 }""")
485 # Min value (default: not checked)
486 var min
: nullable Float
488 # Max value (default: not checked)
489 var max
: nullable Float
491 redef fun validate_field
(v
, obj
) do
492 if not super then return false
493 var val
= obj
.get_or_null
(field
)
495 if required
== null or required
== true then
496 v
.validation
.add_error
(field
, "Expected Float got `null`")
502 if not val
isa Float then
503 v
.validation
.add_error
(field
, "Expected Float got `{val.class_name}`")
507 if min
!= null and val
< min
then
508 v
.validation
.add_error
(field
, "Must be smaller or equal to `{min}`")
512 if max
!= null and val
> max
then
513 v
.validation
.add_error
(field
, "Must be greater or equal to `{max}`")
520 # Check if a field is a Bool
523 # var validator = new ObjectValidator
524 # validator.add new BoolField("field", required=false)
525 # assert validator.validate("""{}""")
526 # assert validator.validate("""{ "field": true }""")
527 # assert validator.validate("""{ "field": false }""")
528 # assert not validator.validate("""{ "field": "foo" }""")
530 # validator = new ObjectValidator
531 # validator.add new BoolField("field")
532 # assert not validator.validate("""{}""")
533 # assert validator.validate("""{ "field": true }""")
534 # assert validator.validate("""{ "field": false }""")
535 # assert not validator.validate("""{ "field": "foo" }""")
538 # No type conversion is applied on the input value:
540 # assert not validator.validate("""{ "field": "true" }""")
541 # assert not validator.validate("""{ "field": 1 }""")
542 # assert not validator.validate("""{ "field": [true] }""")
547 redef fun validate_field
(v
, obj
) do
548 if not super then return false
549 var val
= obj
.get_or_null
(field
)
551 if required
== null or required
== true then
552 v
.validation
.add_error
(field
, "Expected Bool got `null`")
558 if not val
isa Bool then
559 v
.validation
.add_error
(field
, "Expected Bool got `{val.class_name}`")
566 # Check that a field is a JsonObject
569 # var validator = new ObjectValidator
570 # validator.add new RequiredField("id", required = true)
571 # var user_val = new ObjectField("user")
572 # user_val.add new RequiredField("id", required = true)
573 # user_val.add new StringField("login", min_size=4)
574 # validator.add user_val
575 # assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
576 # assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
580 super ObjectValidator
582 redef var validation
= new ValidationResult
584 redef fun validate_field
(v
, obj
) do
585 if not super then return false
586 var val
= obj
.get_or_null
(field
)
588 if required
== null or required
== true then
589 v
.validation
.add_error
(field
, "Expected Object got `null`")
595 var res
= validate_json
(val
)
596 for field
, messages
in validation
.errors
do
597 for message
in messages
do v
.validation
.add_error
("{self.field}.{field}", message
)
603 # Check that a field is a JsonArray
606 # var validator = new ObjectValidator
607 # validator.add new RequiredField("id", required = true)
608 # validator.add new ArrayField("orders", allow_empty=false)
609 # assert not validator.validate("""{ "id": "", "orders": [] }""")
610 # assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
616 autoinit field
=, required
=, allow_empty
=, length
=
618 redef var validation
= new ValidationResult
620 redef fun validate_field
(v
, obj
) do
621 if not super then return false
622 var val
= obj
.get_or_null
(field
)
624 if required
== null or required
== true then
625 v
.validation
.add_error
(field
, "Expected Array got `null`")
631 var res
= validate_json
(val
)
632 for field
, messages
in validation
.errors
do
633 for message
in messages
do v
.validation
.add_error
("{self.field}.{field}", message
)
639 # Check if two fields values match
642 # var validator = new ObjectValidator
643 # validator.add new FieldsMatch("field1", "field2")
645 # assert validator.validate("""{ "field1": {}, "field2": {} }""")
646 # assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
647 # assert validator.validate("""{ "field1": null, "field2": null }""")
648 # assert validator.validate("""{}""")
650 # assert not validator.validate("""{ "field1": {}, "field2": [] }""")
651 # assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
652 # assert not validator.validate("""{ "field1": null, "field2": "" }""")
653 # assert not validator.validate("""{ "field1": "foo" }""")
658 # Other field to compare with
661 redef fun validate_field
(v
, obj
) do
662 var val1
= obj
.get_or_null
(field
)
663 var val2
= obj
.get_or_null
(other
)
665 v
.validation
.add_error
(field
, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
672 # Check if a field match a regular expression
675 # var validator = new ObjectValidator
676 # validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
677 # assert not validator.validate("""{ "title": "foo" }""")
678 # assert validator.validate("""{ "title": "Foo" }""")
683 autoinit field
, re
, required
685 # Regular expression to match
688 redef fun validate_field
(v
, obj
) do
689 if not super then return false
690 var val
= obj
.get_or_null
(field
)
692 if required
== null or required
== true then
693 v
.validation
.add_error
(field
, "Expected String got `null`")
699 if not val
isa String then
700 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
703 if not val
.has
(re
) then
704 v
.validation
.add_error
(field
, "Does not match `{re.string}`")
711 # Check if a field is a valid email
714 # var validator = new ObjectValidator
715 # validator.add new EmailField("email")
716 # assert not validator.validate("""{ "email": "" }""")
717 # assert not validator.validate("""{ "email": "foo" }""")
718 # assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
719 # assert validator.validate("""{ "email": "a+b@c.d" }""")
724 autoinit field
, required
726 redef var re
= "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
729 # Check if a field is a valid ISBN
732 # var validator = new ObjectValidator
733 # validator.add new ISBNField("isbn")
734 # assert not validator.validate("""{ "isbn": "foo" }""")
735 # assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
740 autoinit field
, required
742 redef var re
= "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
745 # Check if a field is a valid URL
747 # Matched against the following regular expression:
749 # ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&:/~\+#]*[a-zA-Z0-9\-\@?^=%&/~\+#])?
751 # You should redefine the base regular expression `re` with your own.
754 # var validator = new ObjectValidator
755 # validator.add new URLField("url")
756 # assert not validator.validate("""{ "url": "" }""")
757 # assert not validator.validate("""{ "url": "foo" }""")
758 # assert not validator.validate("""{ "url": "http://foo" }""")
759 # assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
760 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
761 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
762 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
763 # assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
768 autoinit field
, required
770 redef var re
= "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
773 # Check if a field value is already used
775 # This class provides a stub validator for fields that should contain a unique value along an
776 # application (typically logins or ids).
778 # Here an example that uses a `Repository` if an email is unique:
780 # class UniqueEmailField
783 # var users: UsersRepository
785 # redef fun check_unicity(v, field, val) do
786 # var user = users.find_by_email(val)
787 # if user != null then
788 # v.validation.add_error(field, "Email `{val}` already used")
798 # Check if `val` is already used somewhere
800 # You must redefine this method to handle your own validation.
801 fun check_unicity
(v
: ObjectValidator, field
, val
: String): Bool is abstract
803 redef fun validate_field
(v
, obj
) do
804 if not super then return false
805 var val
= obj
.get_or_null
(field
)
806 if not val
isa String then return false
807 if not check_unicity
(v
, field
, val
) then return false