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_error(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.
99 # See `HttpResponse::json_error`.
100 class ValidationResult
103 # Object parsed during validation
105 # Can be used as a quick way to access the parsed JsonObject instead of
106 # reparsing it during the answer.
108 # See `ObjectValidator`.
109 var object
: nullable JsonObject = null is writable
111 # Array parsed during validation
113 # Can be used as a quick way to access the parsed JsonArray instead of
114 # reparsing it during the answer.
116 # See `ArrayValidator`.
117 var array
: nullable JsonArray = null is writable
119 # Errors found during validation
121 # Errors are grouped by scope.
122 var errors
= new HashMap[String, Array[String]]
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]
129 errors
[scope
].add message
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]
140 # Does `self` contains `errors`?
141 fun has_error
: Bool do return errors
.not_empty
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
)
155 redef fun serialize_to
(v
) do json
.serialize_to
(v
)
157 # Returns the validation result as a pretty formated string
158 fun to_pretty_string
: String do
160 if not has_error
then
161 b
.append
"Everything is correct\n"
163 b
.append
"There is errors\n\n"
164 for k
, v
in errors
do
172 return b
.write_to_string
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 }""")
186 class ObjectValidator
187 super DocumentValidator
189 # Validators to apply on the object
190 var validators
= new Array[FieldValidator]
192 redef fun validate
(document
) do
194 var json
= document
.parse_json
196 validation
.add_error
("document", "Expected JsonObject got `null`")
199 return validate_json
(json
)
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}`")
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
217 fun add
(validator
: FieldValidator) do validators
.add validator
222 # var validator = new ArrayValidator
223 # assert not validator.validate("""{}""")
224 # assert validator.validate("""[]""")
225 # assert validator.validate("""[ "id", 10, {} ]""")
227 # validator = new ArrayValidator(allow_empty=false)
228 # assert not validator.validate("""[]""")
229 # assert validator.validate("""[ "id", 10, {} ]""")
231 # validator = new ArrayValidator(length=3)
232 # assert not validator.validate("""[]""")
233 # assert validator.validate("""[ "id", 10, {} ]""")
236 super DocumentValidator
238 # Allow empty arrays (default: true)
239 var allow_empty
: nullable Bool
241 # Check array length (default: no check)
242 var length
: nullable Int
244 redef fun validate
(document
) do
246 var json
= document
.parse_json
248 validation
.add_error
("document", "Expected JsonArray got `null`")
251 return validate_json
(json
)
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}`")
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")
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}`")
276 # Something that can validate a JsonObject field
277 abstract class FieldValidator
282 # Validate `field` in `obj`
283 fun validate_field
(v
: ObjectValidator, obj
: JsonObject): Bool is abstract
286 # Check if a field exists
289 # var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
290 # var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
291 # var json3 = """{ "field1": "", "field2": "foo" }"""
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)
299 # assert validator.validate(json1)
300 # assert validator.validate(json2)
301 # assert not validator.validate(json3)
302 # assert validator.validation.error("field3") == ["Required field"]
307 # Is this field required?
308 var required
: nullable Bool
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")
320 # Check if a field is a String
322 # `min_size` and `max_size` are optional
325 # var validator = new ObjectValidator
326 # validator.add new StringField("field", required=false)
327 # assert validator.validate("""{}""")
329 # validator = new ObjectValidator
330 # validator.add new StringField("field")
331 # assert not validator.validate("""{}""")
332 # assert not validator.validate("""{ "field": 10 }""")
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": "" }""")
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" }""")
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" }""")
355 # String min size (default: not checked)
356 var min_size
: nullable Int
358 # String max size (default: not checked)
359 var max_size
: nullable Int
361 redef fun validate_field
(v
, obj
) do
362 if not super then return false
363 var val
= obj
.get_or_null
(field
)
365 if required
== null or required
== true then
366 v
.validation
.add_error
(field
, "Expected String got `null`")
372 if not val
isa String then
373 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
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`")
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`")
390 # Check if a field is an Int
393 # var validator = new ObjectValidator
394 # validator.add new IntField("field", required=false)
395 # assert validator.validate("""{}""")
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 }""")
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 }""")
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 }""")
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 }""")
423 # Min value (default: not checked)
424 var min
: nullable Int
426 # Max value (default: not checked)
427 var max
: nullable Int
429 redef fun validate_field
(v
, obj
) do
430 if not super then return false
431 var val
= obj
.get_or_null
(field
)
433 if required
== null or required
== true then
434 v
.validation
.add_error
(field
, "Expected Int got `null`")
440 if not val
isa Int then
441 v
.validation
.add_error
(field
, "Expected Int got `{val.class_name}`")
445 if min
!= null and val
< min
then
446 v
.validation
.add_error
(field
, "Must be greater or equal to `{min}`")
450 if max
!= null and val
> max
then
451 v
.validation
.add_error
(field
, "Must be smaller or equal to `{max}`")
458 # Check if a field is a Float
461 # var validator = new ObjectValidator
462 # validator.add new FloatField("field", required=false)
463 # assert validator.validate("""{}""")
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 }""")
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 }""")
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 }""")
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 }""")
491 # Min value (default: not checked)
492 var min
: nullable Float
494 # Max value (default: not checked)
495 var max
: nullable Float
497 redef fun validate_field
(v
, obj
) do
498 if not super then return false
499 var val
= obj
.get_or_null
(field
)
501 if required
== null or required
== true then
502 v
.validation
.add_error
(field
, "Expected Float got `null`")
508 if not val
isa Float then
509 v
.validation
.add_error
(field
, "Expected Float got `{val.class_name}`")
513 if min
!= null and val
< min
then
514 v
.validation
.add_error
(field
, "Must be smaller or equal to `{min}`")
518 if max
!= null and val
> max
then
519 v
.validation
.add_error
(field
, "Must be greater or equal to `{max}`")
526 # Check that a field is a JsonObject
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" } }""")
540 super ObjectValidator
542 redef var validation
= new ValidationResult
544 redef fun validate_field
(v
, obj
) do
545 if not super then return false
546 var val
= obj
.get_or_null
(field
)
548 if required
== null or required
== true then
549 v
.validation
.add_error
(field
, "Expected Object got `null`")
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
)
563 # Check that a field is a JsonArray
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 ] }""")
576 autoinit field
=, required
=, allow_empty
=, length
=
578 redef var validation
= new ValidationResult
580 redef fun validate_field
(v
, obj
) do
581 if not super then return false
582 var val
= obj
.get_or_null
(field
)
584 if required
== null or required
== true then
585 v
.validation
.add_error
(field
, "Expected Array got `null`")
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
)
599 # Check if two fields values match
602 # var validator = new ObjectValidator
603 # validator.add new FieldsMatch("field1", "field2")
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("""{}""")
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" }""")
618 # Other field to compare with
621 redef fun validate_field
(v
, obj
) do
622 var val1
= obj
.get_or_null
(field
)
623 var val2
= obj
.get_or_null
(other
)
625 v
.validation
.add_error
(field
, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
632 # Check if a field match a regular expression
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" }""")
643 autoinit field
, re
, required
645 # Regular expression to match
648 redef fun validate_field
(v
, obj
) do
649 if not super then return false
650 var val
= obj
.get_or_null
(field
)
652 if required
== null or required
== true then
653 v
.validation
.add_error
(field
, "Expected String got `null`")
659 if not val
isa String then
660 v
.validation
.add_error
(field
, "Expected String got `{val.class_name}`")
663 if not val
.has
(re
) then
664 v
.validation
.add_error
(field
, "Does not match `{re.string}`")
671 # Check if a field is a valid email
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" }""")
684 autoinit field
, required
686 redef var re
= "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
689 # Check if a field is a valid ISBN
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" }""")
700 autoinit field
, required
702 redef var re
= "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re