Validators can be used in Popcorn apps to valid your json inputs before data processing and persistence.
Here an example with a Book management app. We use an ObjectValidator to validate
the books passed to the API in the POST /books
handler.
import popcorn
import popcorn::pop_json
import serialization
# Serializable book representation.
class Book
super Serializable
# Book ISBN
var isbn: String
# Book title
var title: String
# Book image (optional)
var img: nullable String
# Book price
var price: Float
end
class BookValidator
super ObjectValidator
redef init do
add new ISBNField("isbn")
add new StringField("title", min_size=1, max_size=255)
add new StringField("img", required=false)
add new FloatField("price", min=0.0, max=999.0)
end
end
class BookHandler
super Handler
# Insert a new Book
redef fun post(req, res) do
var validator = new BookValidator
if not validator.validate(req.body) then
res.json(validator.validation, 400)
return
end
# TODO data persistence
end
end
Serializable::inspect
to show more useful information
serialization :: serialization_core
Abstract services to serialize Nit objects to different formatscore :: union_find
union–find algorithm using an efficient disjoint-set data structure
# Quick and easy validation framework for Json inputs
#
# Validators can be used in Popcorn apps to valid your json inputs before
# data processing and persistence.
#
# Here an example with a Book management app. We use an ObjectValidator to validate
# the books passed to the API in the `POST /books` handler.
#
# ~~~
# import popcorn
# import popcorn::pop_json
# import serialization
#
# # Serializable book representation.
# class Book
# super Serializable
#
# # Book ISBN
# var isbn: String
#
# # Book title
# var title: String
#
# # Book image (optional)
# var img: nullable String
#
# # Book price
# var price: Float
# end
#
# class BookValidator
# super ObjectValidator
#
# redef init do
# add new ISBNField("isbn")
# add new StringField("title", min_size=1, max_size=255)
# add new StringField("img", required=false)
# add new FloatField("price", min=0.0, max=999.0)
# end
# end
#
# class BookHandler
# super Handler
#
# # Insert a new Book
# redef fun post(req, res) do
# var validator = new BookValidator
# if not validator.validate(req.body) then
# res.json(validator.validation, 400)
# return
# end
# # TODO data persistence
# end
# end
# ~~~
module pop_validation
import json::static
# The base class of all validators
abstract class DocumentValidator
# Validation result
#
# Accessible to the client after the `validate` method has been called.
var validation: ValidationResult is noinit
# Validate the `document` input
#
# Result of the validation can be found in the `validation` attribute.
fun validate(document: String): Bool do
validation = new ValidationResult
return true
end
end
# Validation Result representation
#
# Can be convertted to a JsonObject so it can be reterned in a Json HttpResponse.
#
# Errors messages are grouped into *scopes*. A scope is a string that specify wich
# field or document the error message is related to.
class ValidationResult
super Serializable
# Object parsed during validation
#
# Can be used as a quick way to access the parsed JsonObject instead of
# reparsing it during the answer.
#
# See `ObjectValidator`.
var object: nullable JsonObject = null is writable
# Array parsed during validation
#
# Can be used as a quick way to access the parsed JsonArray instead of
# reparsing it during the answer.
#
# See `ArrayValidator`.
var array: nullable JsonArray = null is writable
# Errors found during validation
#
# Errors are grouped by scope.
var errors = new HashMap[String, Array[String]]
# Generate a new error `message` into `scope`
fun add_error(scope, message: String) do
if not errors.has_key(scope) then
errors[scope] = new Array[String]
end
errors[scope].add message
end
# Get the errors for `scope`
fun error(scope: String): Array[String] do
if not errors.has_key(scope) then
return new Array[String]
end
return errors[scope]
end
# Does `self` contains `errors`?
fun has_error: Bool do return errors.not_empty
redef fun core_serialize_to(v) do
var errors = new JsonObject
for k, e in self.errors do
errors[k] = new JsonArray.from(e)
end
v.serialize_attribute("has_error", has_error)
v.serialize_attribute("errors", errors)
end
# Returns the validation result as a pretty formated string
fun to_pretty_string: String do
var b = new Buffer
if not has_error then
b.append "Everything is correct\n"
else
b.append "There is errors\n\n"
for k, v in errors do
b.append "{k}:\n"
for vv in v do
b.append "\t{vv}\n"
end
b.append "\n"
end
end
return b.write_to_string
end
end
# Check a JsonObject
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# validator.add new StringField("login", min_size=4)
# validator.add new IntField("age", min=0, max=100)
# assert not validator.validate("""{}""")
# assert not validator.validate("""[]""")
# assert validator.validate("""{ "id": "", "login": "Alex", "age": 10 }""")
# ~~~
class ObjectValidator
super DocumentValidator
# Validators to apply on the object
var validators = new Array[FieldValidator]
redef fun validate(document) do
super
var json = document.parse_json
if json == null then
validation.add_error("document", "Expected JsonObject got `null`")
return false
end
return validate_json(json)
end
# Validate a Serializable input
fun validate_json(json: Serializable): Bool do
if not json isa JsonObject then
validation.add_error("document", "Expected JsonObject got `{json.class_name}`")
return false
end
validation.object = json
for validator in validators do
var res = validator.validate_field(self, json)
if not res then return false
end
return true
end
# Add a validator
fun add(validator: FieldValidator) do validators.add validator
end
# Check a JsonArray
# ~~~
# var validator = new ArrayValidator
# assert not validator.validate("""{}""")
# assert validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
#
# validator = new ArrayValidator(allow_empty=false)
# assert not validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
#
# validator = new ArrayValidator(length=3)
# assert not validator.validate("""[]""")
# assert validator.validate("""[ "id", 10, {} ]""")
# ~~~
class ArrayValidator
super DocumentValidator
# Allow empty arrays (default: true)
var allow_empty: nullable Bool
# Check array length (default: no check)
var length: nullable Int
redef fun validate(document) do
super
var json = document.parse_json
if json == null then
validation.add_error("document", "Expected JsonArray got `null`")
return false
end
return validate_json(json)
end
# Validate a Serializable input
fun validate_json(json: Serializable): Bool do
if not json isa JsonArray then
validation.add_error("document", "Expected JsonArray got `{json.class_name}`")
return false
end
validation.array = json
var allow_empty = self.allow_empty
if json.is_empty and (allow_empty != null and not allow_empty) then
validation.add_error("document", "Cannot be empty")
return false
end
var length = self.length
if length != null and json.length != length then
validation.add_error("document", "Array length must be exactly `{length}`")
return false
end
return true
end
end
# Something that can validate a JsonObject field
abstract class FieldValidator
# Field to validate
var field: String
# Validate `field` in `obj`
fun validate_field(v: ObjectValidator, obj: JsonObject): Bool is abstract
end
# Check if a field exists
#
# ~~~
# var json1 = """{ "field1": "", "field2": "foo", "field3": 10, "field4": [] }"""
# var json2 = """{ "field1": "", "field2": "foo", "field3": 10 }"""
# var json3 = """{ "field1": "", "field2": "foo" }"""
#
# var validator = new ObjectValidator
# validator.add new RequiredField("field1")
# validator.add new RequiredField("field2")
# validator.add new RequiredField("field3")
# validator.add new RequiredField("field4", required=false)
#
# assert validator.validate(json1)
# assert validator.validate(json2)
# assert not validator.validate(json3)
# assert validator.validation.error("field3") == ["Required field"]
# ~~~
class RequiredField
super FieldValidator
# Is this field required?
var required: nullable Bool
redef fun validate_field(v, obj) do
var required = self.required
if (required != null and required or required == null) and not obj.has_key(field) then
v.validation.add_error(field, "Required field")
return false
end
return true
end
end
# Check if a field is a String
#
# `min_size` and `max_size` are optional
#
# ~~~
# var validator = new ObjectValidator
# validator.add new StringField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new StringField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": 10 }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", min_size=3)
# assert validator.validate("""{ "field": "foo" }""")
# assert not validator.validate("""{ "field": "fo" }""")
# assert not validator.validate("""{ "field": "" }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", max_size=3)
# assert validator.validate("""{ "field": "foo" }""")
# assert not validator.validate("""{ "field": "fooo" }""")
#
# validator = new ObjectValidator
# validator.add new StringField("field", min_size=3, max_size=5)
# assert not validator.validate("""{ "field": "fo" }""")
# assert validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": "foooo" }""")
# assert not validator.validate("""{ "field": "fooooo" }""")
# ~~~
class StringField
super RequiredField
# String min size (default: not checked)
var min_size: nullable Int
# String max size (default: not checked)
var max_size: nullable Int
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected String got `null`")
return false
else
return true
end
end
if not val isa String then
v.validation.add_error(field, "Expected String got `{val.class_name}`")
return false
end
var min_size = self.min_size
if min_size != null and val.length < min_size then
v.validation.add_error(field, "Must be at least `{min_size} characters long`")
return false
end
var max_size = self.max_size
if max_size != null and val.length > max_size then
v.validation.add_error(field, "Must be at max `{max_size} characters long`")
return false
end
return true
end
end
# Check if a field is an Int
#
# ~~~
# var validator = new ObjectValidator
# validator.add new IntField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new IntField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": 10 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", min=3)
# assert validator.validate("""{ "field": 3 }""")
# assert not validator.validate("""{ "field": 2 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", max=3)
# assert validator.validate("""{ "field": 3 }""")
# assert not validator.validate("""{ "field": 4 }""")
#
# validator = new ObjectValidator
# validator.add new IntField("field", min=3, max=5)
# assert not validator.validate("""{ "field": 2 }""")
# assert validator.validate("""{ "field": 3 }""")
# assert validator.validate("""{ "field": 5 }""")
# assert not validator.validate("""{ "field": 6 }""")
# ~~~
class IntField
super RequiredField
# Min value (default: not checked)
var min: nullable Int
# Max value (default: not checked)
var max: nullable Int
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected Int got `null`")
return false
else
return true
end
end
if not val isa Int then
v.validation.add_error(field, "Expected Int got `{val.class_name}`")
return false
end
var min = self.min
if min != null and val < min then
v.validation.add_error(field, "Must be greater or equal to `{min}`")
return false
end
var max = self.max
if max != null and val > max then
v.validation.add_error(field, "Must be smaller or equal to `{max}`")
return false
end
return true
end
end
# Check if a field is a Float
#
# ~~~
# var validator = new ObjectValidator
# validator.add new FloatField("field", required=false)
# assert validator.validate("""{}""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field")
# assert not validator.validate("""{}""")
# assert not validator.validate("""{ "field": "foo" }""")
# assert validator.validate("""{ "field": 10.5 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", min=3.0)
# assert validator.validate("""{ "field": 3.0 }""")
# assert not validator.validate("""{ "field": 2.0 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", max=3.0)
# assert validator.validate("""{ "field": 3.0 }""")
# assert not validator.validate("""{ "field": 4.0 }""")
#
# validator = new ObjectValidator
# validator.add new FloatField("field", min=3.0, max=5.0)
# assert not validator.validate("""{ "field": 2.0 }""")
# assert validator.validate("""{ "field": 3.0 }""")
# assert validator.validate("""{ "field": 5.0 }""")
# assert not validator.validate("""{ "field": 6.0 }""")
# ~~~
class FloatField
super RequiredField
# Min value (default: not checked)
var min: nullable Float
# Max value (default: not checked)
var max: nullable Float
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected Float got `null`")
return false
else
return true
end
end
if not val isa Float then
v.validation.add_error(field, "Expected Float got `{val.class_name}`")
return false
end
var min = self.min
if min != null and val < min then
v.validation.add_error(field, "Must be smaller or equal to `{min}`")
return false
end
var max = self.max
if max != null and val > max then
v.validation.add_error(field, "Must be greater or equal to `{max}`")
return false
end
return true
end
end
# Check if a field is a Bool
#
# ~~~
# var validator = new ObjectValidator
# validator.add new BoolField("field", required=false)
# assert validator.validate("""{}""")
# assert validator.validate("""{ "field": true }""")
# assert validator.validate("""{ "field": false }""")
# assert not validator.validate("""{ "field": "foo" }""")
#
# validator = new ObjectValidator
# validator.add new BoolField("field")
# assert not validator.validate("""{}""")
# assert validator.validate("""{ "field": true }""")
# assert validator.validate("""{ "field": false }""")
# assert not validator.validate("""{ "field": "foo" }""")
# ~~~
#
# No type conversion is applied on the input value:
# ~~~
# assert not validator.validate("""{ "field": "true" }""")
# assert not validator.validate("""{ "field": 1 }""")
# assert not validator.validate("""{ "field": [true] }""")
# ~~~
class BoolField
super RequiredField
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected Bool got `null`")
return false
else
return true
end
end
if not val isa Bool then
v.validation.add_error(field, "Expected Bool got `{val.class_name}`")
return false
end
return true
end
end
# Check that a field is a JsonObject
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# var user_val = new ObjectField("user")
# user_val.add new RequiredField("id", required = true)
# user_val.add new StringField("login", min_size=4)
# validator.add user_val
# assert not validator.validate("""{ "id": "", "user": { "login": "Alex" } }""")
# assert validator.validate("""{ "id": "", "user": { "id": "foo", "login": "Alex" } }""")
# ~~~
class ObjectField
super RequiredField
super ObjectValidator
redef var validation = new ValidationResult
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected Object got `null`")
return false
else
return true
end
end
var res = validate_json(val)
for field, messages in validation.errors do
for message in messages do v.validation.add_error("{self.field}.{field}", message)
end
return res
end
end
# Check that a field is a JsonArray
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RequiredField("id", required = true)
# validator.add new ArrayField("orders", allow_empty=false)
# assert not validator.validate("""{ "id": "", "orders": [] }""")
# assert validator.validate("""{ "id": "", "orders": [ 1 ] }""")
# ~~~
class ArrayField
super RequiredField
super ArrayValidator
autoinit field=, required=, allow_empty=, length=
redef var validation = new ValidationResult
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected Array got `null`")
return false
else
return true
end
end
var res = validate_json(val)
for field, messages in validation.errors do
for message in messages do v.validation.add_error("{self.field}.{field}", message)
end
return res
end
end
# Check if two fields values match
#
# ~~~
# var validator = new ObjectValidator
# validator.add new FieldsMatch("field1", "field2")
#
# assert validator.validate("""{ "field1": {}, "field2": {} }""")
# assert validator.validate("""{ "field1": "foo", "field2": "foo" }""")
# assert validator.validate("""{ "field1": null, "field2": null }""")
# assert validator.validate("""{}""")
#
# assert not validator.validate("""{ "field1": {}, "field2": [] }""")
# assert not validator.validate("""{ "field1": "foo", "field2": "bar" }""")
# assert not validator.validate("""{ "field1": null, "field2": "" }""")
# assert not validator.validate("""{ "field1": "foo" }""")
# ~~~
class FieldsMatch
super FieldValidator
# Other field to compare with
var other: String
redef fun validate_field(v, obj) do
var val1 = obj.get_or_null(field)
var val2 = obj.get_or_null(other)
if val1 != val2 then
v.validation.add_error(field, "Values mismatch: `{val1 or else "null"}` against `{val2 or else "null"}`")
return false
end
return true
end
end
# Check if a field match a regular expression
#
# ~~~
# var validator = new ObjectValidator
# validator.add new RegexField("title", "[A-Z][a-z]+".to_re)
# assert not validator.validate("""{ "title": "foo" }""")
# assert validator.validate("""{ "title": "Foo" }""")
# ~~~
class RegexField
super RequiredField
autoinit field, re, required
# Regular expression to match
var re: Regex
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if val == null then
if required == null or required == true then
v.validation.add_error(field, "Expected String got `null`")
return false
else
return true
end
end
if not val isa String then
v.validation.add_error(field, "Expected String got `{val.class_name}`")
return false
end
if not val.has(re) then
v.validation.add_error(field, "Does not match `{re.string}`")
return false
end
return true
end
end
# Check if a field is a valid email
#
# ~~~
# var validator = new ObjectValidator
# validator.add new EmailField("email")
# assert not validator.validate("""{ "email": "" }""")
# assert not validator.validate("""{ "email": "foo" }""")
# assert validator.validate("""{ "email": "alexandre@moz-code.org" }""")
# assert validator.validate("""{ "email": "a+b@c.d" }""")
# ~~~
class EmailField
super RegexField
autoinit field, required
redef var re = "(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9.-]+$)".to_re
end
# Check if a field is a valid ISBN
#
# ~~~
# var validator = new ObjectValidator
# validator.add new ISBNField("isbn")
# assert not validator.validate("""{ "isbn": "foo" }""")
# assert validator.validate("""{ "isbn": "ISBN 0-596-00681-0" }""")
# ~~~
class ISBNField
super RegexField
autoinit field, required
redef var re = "(^ISBN [0-9]-[0-9]\{3\}-[0-9]\{5\}-[0-9]?$)".to_re
end
# Check if a field is a valid URL
#
# Matched against the following regular expression:
# ~~~raw
# ^(http|https):\/\/[a-zA-Z0-9\-_]+(\.[a-zA-Z0-9\-_]+)+([a-zA-Z0-9\-\.,@?^=%&:/~\+#]*[a-zA-Z0-9\-\@?^=%&/~\+#])?
# ~~~
# You should redefine the base regular expression `re` with your own.
#
# ~~~
# var validator = new ObjectValidator
# validator.add new URLField("url")
# assert not validator.validate("""{ "url": "" }""")
# assert not validator.validate("""{ "url": "foo" }""")
# assert not validator.validate("""{ "url": "http://foo" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a" }""")
# assert validator.validate("""{ "url": "http://nitlanguage.org/foo?q&a=1" }""")
# ~~~
class URLField
super RegexField
autoinit field, required
redef var re = "^(http|https):\\/\\/[a-zA-Z0-9\\-_]+(\\.[a-zA-Z0-9\\-_]+)+([a-zA-Z0-9\\-\\.,@?^=%&:/~\\+#]*[a-zA-Z0-9\\-\\@?^=%&/~\\+#])?".to_re
end
# Check if a field value is already used
#
# This class provides a stub validator for fields that should contain a unique value along an
# application (typically logins or ids).
#
# Here an example that uses a `Repository` if an email is unique:
# ~~~nitish
# class UniqueEmailField
# super UniqueField
#
# var users: UsersRepository
#
# redef fun check_unicity(v, field, val) do
# var user = users.find_by_email(val)
# if user != null then
# v.validation.add_error(field, "Email `{val}` already used")
# return false
# end
# return true
# end
# end
# ~~~
class UniqueField
super StringField
# Check if `val` is already used somewhere
#
# You must redefine this method to handle your own validation.
fun check_unicity(v: ObjectValidator, field, val: String): Bool is abstract
redef fun validate_field(v, obj) do
if not super then return false
var val = obj.get_or_null(field)
if not val isa String then return false
if not check_unicity(v, field, val) then return false
return true
end
end
lib/popcorn/pop_validation.nit:17,1--810,3