Kontrol is a DSL to define validations for JSON data.
Add this to your application's shard.yml
:
dependencies:
kontrol:
github: ragmaanir/kontrol
require "kontrol"
# To define a validation you can use the `Kontrol.object` macro.
# Validations can be defined by expressions:
val, _ = Kontrol.object(
name: {type: String, length: v.size > 4},
)
# When the constraint `length` is violated it returns:
j = json(name: "a")
assert val.call(j) == {"name" => [:length]}
# The value of the property `name` can be accessed via `v` in the validation expression.
# For the type validation there are two shortcuts since they are so common:
# Shortcut 1
Kontrol.object(
name: {type: String},
)
# Shortcut 2
Kontrol.object(
name: String,
)
# You always have to specify the type (might change).
# You can also define root-level validations for an object:
val, _ = Kontrol.object(
{length: v["name"].as_s.size == v["name_length"].as_i},
name: String,
name_length: Int64
)
# The `length` validation checks whether the length of the name string matches the name_length.
# The root-level validations are only executed when all properties are valid to prevent
# them raising exceptions caused by invalid data. A root level validation error
# is stored under the key "@", because the object might not have a name. So in this case:
j = json(
name: "test",
name_length: 3
)
assert val.call(j) == {
"@" => [:length],
}
Simple example:
# First object is the validator, second is the converter that can be used
# to convert the JSON to nested named tuples.
val, con = Kontrol.object(
name: String,
percentage: {type: Int64, min: v > 0, max: v <= 100}
)
# invalid since percentage violates :min constraint
assert val.call(json(
name: "test",
percentage: -1
)) == {"percentage" => [:min]}
# invalid since name violates :type-constraint and :min is violated
assert val.call(json(
name: 2,
percentage: -1
)) == {"name" => [:type], "percentage" => [:min]}
# invalid since the required attributes are missing or nil
assert val.call(json(
name: nil
)) == {"name" => [:required], "percentage" => [:required]}
# valid
assert val.call(json(
name: "test",
percentage: 45
)).empty?
# convert input to nested named tuples
assert con.call(json(
name: "test",
percentage: 45
)) == {name: "test", percentage: 45}
Advanced example (nested objects and root-level-validations):
val, _ = object(
{
my_book: v["author"].as_s == v["book"]["author"].as_s,
},
author: String,
book: object(
author: String
)
)
assert val.call(json(
author: "Bob"
)) == {"book" => [:required]}
assert val.call(json(
author: "Bob",
book: {
author: 1337,
}
)) == {"book.author" => [:type]}
assert val.call(json(
author: "Bob",
book: {
author: "Bobby",
}
)) == {"@" => [:my_book]}
assert val.call(json(
author: "Bob",
book: {
author: "Bob",
}
)).empty?
- Support arrays
- Support/test required/optional attributes
- Consistent handling of unspecified attributes (reject? ignore? errors?)
- Cleanup/simplify macros
- Use rule-class instances instead of closures?
- Fork it ( https://github.com/ragmaanir/kontrol/fork )
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create a new Pull Request
- ragmaanir - creator, maintainer