Add docsite for version 1.0
This commit is contained in:
parent
95e3feec31
commit
0263e85840
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
title: Advanced
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
sections:
|
||||
- predicate-logic
|
||||
- filtering
|
||||
- key-maps
|
||||
- rule-ast
|
||||
- custom-types
|
||||
---
|
||||
|
||||
- [Predicate Logic](/gems/dry-schema/advanced/predicate-logic)
|
||||
- [Filtering](/gems/dry-schema/advanced/filtering)
|
||||
- [Key maps](/gems/dry-schema/advanced/key-maps)
|
||||
- [Rule AST](/gems/dry-schema/advanced/rule-ast)
|
||||
- [Custom types](/gems/dry-schema/advanced/custom-types)
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Custom types
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
You would often use `dry-schema` to transform your input. Let's say, you want to remove any whitespace characters at the beginning and the end of the string. You would probably make a type like this and use it in your schema:
|
||||
|
||||
```ruby
|
||||
StrippedString = Types::String.constructor(&:strip)
|
||||
|
||||
Schema = Dry::Schema.Params do
|
||||
required(:some_number).filled(:integer)
|
||||
required(:my_string).filled(StrippedString)
|
||||
end
|
||||
```
|
||||
|
||||
However, you might find it inconvenient to use constants instead of symbols, especially if you want to use the type throughout the project. You might want to use it like this:
|
||||
|
||||
```ruby
|
||||
Schema = Dry::Schema.Params do
|
||||
required(:some_number).filled(:integer)
|
||||
required(:my_string).filled(:stripped_string)
|
||||
end
|
||||
```
|
||||
|
||||
Version `1.2` introduced a solution that would let you achieve that. You'll need to build a custom type container — an instance `Dry::Schema::TypeContainer`, register your types, and pass it to `config.types`.
|
||||
|
||||
```ruby
|
||||
StrippedString = Types::String.constructor(&:strip)
|
||||
|
||||
TypeContainer = Dry::Schema::TypeContainer.new
|
||||
TypeContainer.register('params.stripped_string', StrippedString)
|
||||
|
||||
Schema = Dry::Schema.Params do
|
||||
config.types = TypeContainer
|
||||
required(:some_number).filled(:integer)
|
||||
required(:my_string).filled(:stripped_string)
|
||||
end
|
||||
```
|
||||
|
||||
Each schema processor uses different namespaces, so you'll have to keep it in mind when chosing the key.
|
||||
|
||||
- Use `params.my_type` if you want to register the type for `Dry::Schema::Params`
|
||||
- Use `nominal.my_type` if you want to register the type for `Dry::Schema::Processor`
|
||||
- Use `json.my_type` if you want to register the type for `Dry::Schema::JSON`
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
title: Filtering
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
One of the unique features of `dry-schema` is the ability to define rules that are applied **before coercion**. This means you can ensure that **the input** has the right format for the coercion to work, or maybe you just want to restrict certain values entering your system.
|
||||
|
||||
Here's a common example - your system supports only one specific date format, and you want to validate that the input value has this format before trying to coerce it into a date object. In order to do that, you can use `filter` macro, which works just like `value`, but its predicates are applied before value coercion:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled
|
||||
required(:birthday).filter(format?: /\d{4}-\d{2}-\d{2}/).value(:date)
|
||||
end
|
||||
|
||||
schema.call('email' => 'jane@doe.org', 'birthday' => '1981-1-1')
|
||||
#<Dry::Schema::Result{:email=>"jane@doe.org", :birthday=>"1981-1-1"} errors={:birthday=>["is in invalid format"]}>
|
||||
|
||||
schema.call('email' => 'jane@doe.org', 'birthday' => '1981-01-01')
|
||||
#<Dry::Schema::Result{:email=>"jane@doe.org", :birthday=>#<Date: 1981-01-01 ((2444606j,0s,0n),+0s,2299161j)>} errors={}>
|
||||
```
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: Key maps
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
When you define a schema, you get access to the key map which holds information about specified keys. Internally, `dry-schema` uses key maps to:
|
||||
|
||||
* Rebuild the original input hash by rejecting unknown keys
|
||||
* (optional) coerce keys from strings to symbols
|
||||
|
||||
### Accessing key map
|
||||
|
||||
To access schema's key map use `Schema#key_map` method:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
optional(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
schema.key_map
|
||||
# => #<Dry::Schema::KeyMap["email", "age"]>
|
||||
|
||||
schema.key_map.write("email" => "jane@doe.org", "age" => 21, "something_unexpected" => "oops")
|
||||
# => {:email=>"jane@doe.org", :age=>21}
|
||||
```
|
||||
|
||||
### KeyMap is an enumerable
|
||||
|
||||
You can use `Enumerable` API when working with key maps:
|
||||
|
||||
``` ruby
|
||||
schema.key_map.each { |key| puts key.inspect }
|
||||
# #<Dry::Schema::Key name="email" coercer=#<Proc:0x00007feb288ff848(&:to_sym)>>
|
||||
# #<Dry::Schema::Key name="age" coercer=#<Proc:0x00007feb288ff848(&:to_sym)>>
|
||||
|
||||
schema.key_map.detect { |key| key.name.eql?("email") }
|
||||
# => #<Dry::Schema::Key name="email" coercer=#<Proc:0x00007feb288ff848(&:to_sym)>>
|
||||
```
|
||||
|
||||
### Learn more
|
||||
|
||||
- [`KeyMap` API documentation](https://www.rubydoc.info/gems/dry-schema/Dry/Schema/KeyMap)
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
title: Predicate logic
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
Schema DSL allows you to define validation rules using predicate logic. All common logic operators are supported and you can use them to **compose rules**. This simple technique is very powerful as it allows you to compose validations in such a way that invalid state will not crash one of your rules. Validation is a process that always depends on specific conditions, in that sense, `dry-schema` schemas have rules that are always conditional, they are executed only if defined conditions are met.
|
||||
|
||||
This document explains how rule composition works in terms of predicate logic.
|
||||
|
||||
### Conjunction (and)
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
required(:age) { int? & gt?(18) }
|
||||
end
|
||||
```
|
||||
|
||||
`:age` rule is successful when both predicates return `true`.
|
||||
|
||||
### Disjunction (or)
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
required(:age) { none? | int? }
|
||||
end
|
||||
```
|
||||
|
||||
`:age` rule is successful when either of the predicates, or both return `true`.
|
||||
|
||||
### Implication (then)
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
required(:age) { filled? > int? }
|
||||
end
|
||||
```
|
||||
|
||||
`:age` rule is successful when `filled?` returns `false`, or when both predicates return `true`.
|
||||
|
||||
> [Optional keys](/gems/dry-schema/optional-keys-and-values) are defined using `implication`, that's why a missing key will not cause its rules to be applied and the whole key rule will be successful
|
||||
|
||||
### Exclusive Disjunction (xor)
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
required(:status).value(:integer) { even? ^ lt?(0) }
|
||||
end
|
||||
```
|
||||
|
||||
`:status` is valid if it's either an even integer, or it's value is less than `0`.
|
||||
|
||||
### Operator Aliases
|
||||
|
||||
Logic operators are actually aliases, use full method names at your own convenience:
|
||||
|
||||
- `and` => `&`
|
||||
- `or` => `|`
|
||||
- `then` => `>`
|
||||
- `xor` => `^`
|
|
@ -0,0 +1,104 @@
|
|||
---
|
||||
title: Rule AST
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
The DSL in `dry-schema` is used to create rule objects that are provided by [`dry-logic`](/gems/dry-logic). These rules are built using an AST, which uses simple data structures to represent predicates and how they are composed into complex rules and operations.
|
||||
|
||||
The AST can be used to convert it into another representation - for example meta-data that can be used to produce documentation.
|
||||
|
||||
### Accessing the AST
|
||||
|
||||
To access schema's rule AST use `Schema#to_ast` method:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
optional(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
schema.to_ast
|
||||
# => [:set,
|
||||
# [[:and,
|
||||
# [[:predicate, [:key?, [[:name, :email], [:input, Undefined]]]],
|
||||
# [:key, [:email, [:and, [[:predicate, [:str?, [[:input, Undefined]]]], [:predicate, [:filled?, [[:input, Undefined]]]]]]]]]],
|
||||
# [:implication,
|
||||
# [[:predicate, [:key?, [[:name, :age], [:input, Undefined]]]],
|
||||
# [:key,
|
||||
# [:age,
|
||||
# [:and,
|
||||
# [[:and, [[:predicate, [:int?, [[:input, Undefined]]]], [:predicate, [:filled?, [[:input, Undefined]]]]]],
|
||||
# [:predicate, [:gt?, [[:num, 18], [:input, Undefined]]]]]]]]]]]]
|
||||
```
|
||||
|
||||
### Writing an AST compiler
|
||||
|
||||
Even though such a data structure may look scary, it's actually very easy to write a compiler that will turn it into something useful. Let's say you want to generate meta-data about the schema and use it for documentation purposes. To do this, you can write an AST compiler.
|
||||
|
||||
Here's a simple example to give you the idea:
|
||||
|
||||
```ruby
|
||||
require 'dry/schema'
|
||||
|
||||
class DocCompiler
|
||||
def visit(node)
|
||||
meth, rest = node
|
||||
public_send(:"visit_#{meth}", rest)
|
||||
end
|
||||
|
||||
def visit_set(nodes)
|
||||
nodes.map { |node| visit(node) }.flatten(1)
|
||||
end
|
||||
|
||||
def visit_and(node)
|
||||
left, right = node
|
||||
[visit(left), visit(right)].compact
|
||||
end
|
||||
|
||||
def visit_key(node)
|
||||
name, rest = node
|
||||
|
||||
predicates = visit(rest).flatten(1).reduce(:merge)
|
||||
validations = predicates.map { |name, args| predicate_description(name, args) }.compact
|
||||
|
||||
{ key: name, validations: validations }
|
||||
end
|
||||
|
||||
def visit_implication(node)
|
||||
_, right = node.map(&method(:visit))
|
||||
right.merge(optional: true)
|
||||
end
|
||||
|
||||
def visit_predicate(node)
|
||||
name, args = node
|
||||
|
||||
return if name.equal?(:key?)
|
||||
|
||||
{ name => args.map(&:last).reject { |v| v.equal?(Dry::Schema::Undefined) } }
|
||||
end
|
||||
|
||||
def predicate_description(name, args)
|
||||
case name
|
||||
when :str? then "must be a string"
|
||||
when :filled? then "must be filled"
|
||||
when :int? then "must be an integer"
|
||||
when :gt? then "must be greater than #{args[0]}"
|
||||
else
|
||||
raise NotImplementedError, "#{name} not supported yet"
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
With such a compiler we can now turn schema's rule AST into a list of hashes that describe keys and their validations:
|
||||
|
||||
``` ruby
|
||||
compiler = DocCompiler.new
|
||||
|
||||
compiler.visit(schema.to_ast)
|
||||
# [
|
||||
# {:key=>:email, :validations=>["must be filled", "must be a string"]},
|
||||
# {:key=>:age, :validations=>["must be filled", "must be an integer", "must be greater than 18"], :optional=>true}
|
||||
# ]
|
||||
```
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: Basics
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
sections:
|
||||
- macros
|
||||
- type-specs
|
||||
- built-in-predicates
|
||||
- working-with-schemas
|
||||
---
|
||||
|
||||
Here's a basic example where we validate the following things:
|
||||
|
||||
- The input _must have a key_ called `:email`
|
||||
- Provided the email key is present, its value _must be filled_
|
||||
- The input _must have a key_ called `:age`
|
||||
- Provided the age key is present, its value _must be an integer_ and it _must be greater than 18_
|
||||
|
||||
This can be easily expressed through the DSL:
|
||||
|
||||
```ruby
|
||||
require 'dry-schema'
|
||||
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
schema.call(email: 'jane@doe.org', age: 19)
|
||||
# #<Dry::Schema::Result{:email=>"jane@doe.org", :age=>19} errors={}>
|
||||
|
||||
schema.call("email" => "", "age" => "19")
|
||||
# #<Dry::Schema::Result{:email=>"", :age=>19} errors={:email=>["must be filled"]}>
|
||||
```
|
||||
|
||||
When you apply this schema to an input, 3 things happen:
|
||||
|
||||
1. Input keys are coerced to symbols using schema's key map
|
||||
2. Input values are coerced based on type specs
|
||||
3. Input keys and values are validated using defined schema rules
|
||||
|
||||
### Learn more
|
||||
|
||||
- [Macros](/gems/dry-schema/basics/macros)
|
||||
- [Type specs](/gems/dry-schema/basics/type-specs)
|
||||
- [Built-in predicates](/gems/dry-schema/basics/built-in-predicates)
|
||||
- [Working with schemas](/gems/dry-schema/basics/working-with-schemas)
|
|
@ -0,0 +1,486 @@
|
|||
---
|
||||
title: Built-in predicates
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
The DSL supports many built-in predicates that can be used to verify validity of the input. If the predicates do not meet your requirements, you probably want to look at [dry-validation](/gems/dry-validation) which offers a more advanced way of defining validations.
|
||||
|
||||
### `nil?`
|
||||
|
||||
Checks that a key's value is nil.
|
||||
|
||||
```ruby
|
||||
describe 'nil?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(:nil?)
|
||||
end
|
||||
end
|
||||
|
||||
let(:input) { {sample: nil} }
|
||||
|
||||
it 'as regular ruby' do
|
||||
assert input[:sample].nil?
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(input).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `eql?`
|
||||
|
||||
Checks that a key's value is equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'eql?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(eql?: 1234)
|
||||
end
|
||||
end
|
||||
|
||||
let(:input) { {sample: 1234} }
|
||||
|
||||
it 'as regular ruby' do
|
||||
assert input[:sample] == 1234
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(input).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
### `type?`
|
||||
|
||||
Checks that a key's class is equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'type?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(type?: Integer)
|
||||
end
|
||||
end
|
||||
|
||||
let(:input) { {sample: 1234} }
|
||||
|
||||
it 'as regular ruby' do
|
||||
assert input[:sample].class == Integer
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(input).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Shorthand for common Ruby types:
|
||||
|
||||
- `str?` equivalent to `type?(String)`
|
||||
- `int?` equivalent to `type?(Integer)`
|
||||
- `float?` equivalent to `type?(Float)`
|
||||
- `decimal?` equivalent to `type?(BigDecimal)`
|
||||
- `bool?` equivalent to `type?(Boolean)`
|
||||
- `date?` equivalent to `type?(Date)`
|
||||
- `time?` equivalent to `type?(Time)`
|
||||
- `date_time?` equivalent to `type?(DateTime)`
|
||||
- `array?` equivalent to `type?(Array)`
|
||||
- `hash?` equivalent to `type?(Hash)`
|
||||
|
||||
## Number, String, Collection
|
||||
|
||||
### `empty?`
|
||||
|
||||
Checks that either the array, string, or hash is empty.
|
||||
|
||||
```ruby
|
||||
describe 'empty?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(:empty?)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert {sample: ""}[:sample].empty?
|
||||
assert {sample: []}[:sample].empty?
|
||||
assert {sample: {}}[:sample].empty?
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: "").success?
|
||||
assert schema.call(sample: []).success?
|
||||
assert schema.call(sample: {}).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `filled?`
|
||||
|
||||
Checks that either the value is non-nil and, in the case of a String, Hash, or Array, non-empty.
|
||||
|
||||
```ruby
|
||||
describe 'filled?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(:filled?)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert !{sample: "1"}[:sample].empty?
|
||||
assert !{sample: [2]}[:sample].empty?
|
||||
assert !{sample: {k: 3}}[:sample].empty?
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: "1").success?
|
||||
assert schema.call(sample: [2]).success?
|
||||
assert schema.call(sample: {k: 3}).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `gt?`
|
||||
|
||||
Checks that the value is greater than the given value.
|
||||
|
||||
```ruby
|
||||
describe 'gt?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(gt?: 0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 1 > 0
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 1).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `gteq?`
|
||||
|
||||
Checks that the value is greater than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'gteq?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(gteq?: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 1 >= 1
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 1).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `lt?`
|
||||
|
||||
Checks that the value is less than the given value.
|
||||
|
||||
```ruby
|
||||
describe 'lt?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(lt?: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 0 < 1
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 0).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `lteq?`
|
||||
|
||||
Checks that the value is less than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'lteq?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(lteq?: 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 1 <= 1
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 1).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `max_size?`
|
||||
|
||||
Check that an array's size (or a string's length) is less than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'max_size?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(max_size?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert [1, 2, 3].size <= 3
|
||||
assert 'foo'.size <= 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: [1, 2, 3]).success?
|
||||
assert schema.call(sample: 'foo').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `min_size?`
|
||||
|
||||
Checks that an array's size (or a string's length) is greater than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'min_size?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(min_size?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert [1, 2, 3].size >= 3
|
||||
assert 'foo'.size >= 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: [1, 2, 3]).success?
|
||||
assert schema.call(sample: 'foo').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `size?(int)`
|
||||
|
||||
Checks that an array's size (or a string's length) is equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'size?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(size?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert [1, 2, 3].size == 3
|
||||
assert 'foo'.size == 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: [1, 2, 3]).success?
|
||||
assert schema.call(sample: 'foo').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `size?(range)`
|
||||
|
||||
Checks that an array's size (or a string's length) is within a range of values.
|
||||
|
||||
```ruby
|
||||
describe 'size?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(size?: 0..3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert (0..3).include?([1, 2, 3].size)
|
||||
assert (0..3).include?('foo'.size)
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: [1, 2, 3]).success?
|
||||
assert schema.call(sample: 'foo').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `max_bytesize?`
|
||||
|
||||
String's bytesize is less than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'max_bytesize?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(max_bytesize?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 'こ'.byte <= 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 'こ').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `min_bytesize?`
|
||||
|
||||
String's bytesize is greater than or equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'min_binsize?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(min_bytesize?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 'こ'.byte <= 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 'こ').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `bytesize?(int)`
|
||||
|
||||
Checks that an array's size (or a string's length) is equal to the given value.
|
||||
|
||||
```ruby
|
||||
describe 'bytesize?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(bytesize?: 3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert 'こ'.byte <= 3
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 'こ').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `bytesize?(range)`
|
||||
|
||||
Checks that an array's size (or a string's length) is within a range of values.
|
||||
|
||||
```ruby
|
||||
describe 'bytesize?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(bytesize?: 0..3)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert (0..3).include?('こ'.size)
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 'こ').success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `format?`
|
||||
|
||||
Checks that a string matches a given regular expression.
|
||||
|
||||
```ruby
|
||||
describe 'format?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(format?: /^a/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert /^a/ =~ "aa"
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: "aa").success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `included_in?`
|
||||
|
||||
Checks that a value is included in a given array.
|
||||
|
||||
```ruby
|
||||
describe 'included_in?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(included_in?: [1,3,5])
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert [1,3,5].include?(3)
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 3).success?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `excluded_from?`
|
||||
|
||||
Checks that a value is excluded from a given array.
|
||||
|
||||
```ruby
|
||||
describe 'excluded_from?' do
|
||||
let(:schema) do
|
||||
Dry::Schema.Params do
|
||||
required(:sample).value(excluded_from?: [1,3,5])
|
||||
end
|
||||
end
|
||||
|
||||
it 'with regular ruby' do
|
||||
assert ![1,3,5].include?(2)
|
||||
end
|
||||
|
||||
it 'with dry-schema' do
|
||||
assert schema.call(sample: 2).success?
|
||||
end
|
||||
end
|
||||
```
|
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
title: Macros
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
Defining rules using blocks is very flexible and powerful; however, in most common cases repeatedly defining the same rules leads to boilerplate code. That's why `dry-schema`'s DSL provides convenient macros to reduce that boilerplate. Every macro can be expanded to its block-based equivalent.
|
||||
|
||||
This document describes available built-in macros.
|
||||
|
||||
### `value`
|
||||
|
||||
Use it to quickly provide a list of all predicates that will be `AND`-ed automatically:
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to `required(:age) { int? & gt?(18) }`
|
||||
required(:age).value(:integer, gt?: 18)
|
||||
end
|
||||
```
|
||||
|
||||
### `filled`
|
||||
|
||||
Use it when a value is expected to be filled. "filled" means that the value is non-nil and, in the case of a `String`, `Hash`, or `Array` value, that the value is not `.empty?`.
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to `required(:age) { int? & filled? }`
|
||||
required(:age).filled(:integer)
|
||||
end
|
||||
```
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to `required(:age) { array? & filled? }`
|
||||
required(:tags).filled(:array)
|
||||
end
|
||||
```
|
||||
|
||||
### `maybe`
|
||||
|
||||
Use it when a value can be nil.
|
||||
|
||||
> Notice: do not confuse `maybe` with the `optional` method, which allows **a key to be omitted in the input**, whereas `maybe` is for checking **the value**
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to `required(:age) { !nil?.then(int?) }`
|
||||
required(:age).maybe(:integer)
|
||||
end
|
||||
```
|
||||
|
||||
### `hash`
|
||||
|
||||
Use it when a value is expected to be a hash:
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to: `required(:tags) { hash? & filled? & schema { required(:name).filled(:string) } } }`
|
||||
required(:tags).hash do
|
||||
required(:name).filled(:string)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `schema`
|
||||
|
||||
This works like `hash` but does not prepend `hash?` predicate. It's a simpler building block for checking nested hashes. Use it when *you* want to provide base checks prior applying rules to values.
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to: `required(:tags) { hash? & filled? & schema { required(:name).filled(:string) } } }`
|
||||
required(:tags).filled(:hash).schema do
|
||||
required(:name).filled(:string)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `array`
|
||||
|
||||
Use it to apply predicates to every element in a value that is expected to be an array.
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to: `required(:tags) { array? & each { str? } } }`
|
||||
required(:tags).array(:str?)
|
||||
end
|
||||
```
|
||||
|
||||
You can also define an array where elements are hashes:
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to: `required(:tags) { array? & each { hash { required(:name).filled(:string) } } } }`
|
||||
required(:tags).array(:hash) do
|
||||
required(:name).filled(:string)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### `each`
|
||||
|
||||
This works like `array` but does not prepend `array?` predicate. It's a simpler building block for checking each element of an array. Use it when *you* want to provide base checks prior applying rules to elements.
|
||||
|
||||
```ruby
|
||||
Dry::Schema.Params do
|
||||
# expands to: `required(:tags) { array? & each { str? } } }`
|
||||
required(:tags).value(:array, min_size?: 2).each(:str?)
|
||||
end
|
||||
```
|
||||
|
||||
### Learn more
|
||||
|
||||
- [Type specs](/gems/dry-schema/basics/type-specs)
|
||||
- [Built-in predicates](/gems/dry-schema/basics/built-in-predicates)
|
||||
- [Working with schemas](/gems/dry-schema/basics/working-with-schemas)
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
title: Type specs
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
To define what the expected type of a value is, you should use type specs. All macros support type specs as the first argument, whenever you pass a symbol that doesn't end with a question mark, or you explicitly pass in an instance of a `Dry::Types::Type` object, it will be set as the type.
|
||||
|
||||
> Whenever you define a type spec, `dry-schema` will infer a type-check predicate. ie:
|
||||
> * `:string` => `str?`
|
||||
> * `:integer` => `:int?`
|
||||
> * `:array` => `:array?`
|
||||
> * etc.
|
||||
>
|
||||
> These predicates will be *prepended* to the list of the predicates you specified (if any).
|
||||
|
||||
### Using type identifiers
|
||||
|
||||
In most common cases you can use symbols that identify built-in types. The types are resolved from type registry which is configured for individual schemas. For example `Dry::Schema::Params` has its type registry configured to use `Params` types by default. This means that if you specify `:integer` as the type, then `Dry::Schema::Types::Params::Integer` will be used as the resolved type.
|
||||
|
||||
```ruby
|
||||
UserSchema = Dry::Schema.Params do
|
||||
# expands to: `int? & gt?(18)`
|
||||
required(:age).value(:integer, gt?: 18)
|
||||
end
|
||||
```
|
||||
|
||||
### Using arrays with member types
|
||||
|
||||
To define an array with a member, you can use a shortcut method `array`. Here's an example of an array with `:integer` set as its member type:
|
||||
|
||||
``` ruby
|
||||
UserSchema = Dry::Schema.Params do
|
||||
# expands to: `array? & each { int? } & size?(3)`
|
||||
required(:nums).value(array[:integer], size?: 3)
|
||||
end
|
||||
```
|
||||
|
||||
### Using custom types
|
||||
|
||||
You are not limited to the built-in types. The DSL accepts any `Dry::Types::Type` object:
|
||||
|
||||
```ruby
|
||||
module Types
|
||||
include Dry::Types()
|
||||
|
||||
StrippedString = Types::String.constructor(&:strip)
|
||||
end
|
||||
|
||||
UserSchema = Dry::Schema.Params do
|
||||
# expands to: `str? & min_size?(10)`
|
||||
required(:login_time).value(StrippedString, min_size?: 10)
|
||||
end
|
||||
```
|
||||
|
||||
### Learn more
|
||||
|
||||
- [Built-in predicates](/gems/dry-schema/basics/built-in-predicates)
|
||||
- [Working with schemas](/gems/dry-schema/basics/working-with-schemas)
|
|
@ -0,0 +1,112 @@
|
|||
---
|
||||
title: Working with schemas
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
A schema is an object which contains a list of rules that will be applied to its input when you call a schema. It returns a `result object` which provides an API to retrieve `error messages` and access to the validation output.
|
||||
|
||||
Schema definition best practices:
|
||||
|
||||
- Be specific about the exact shape of the data, define all the keys that you expect to be present
|
||||
- Specify optional keys too, even if you don't need additional rules to be applied to their values
|
||||
- **Specify type specs** for all the values
|
||||
- Assign schema objects to constants for convenient access
|
||||
- Define a base schema for your application with common configuration
|
||||
|
||||
### Calling a schema
|
||||
|
||||
Calling a schema will apply all its rules to the input. High-level rules defined with the `rule` API are applied in a second step and they are guarded, which means if the values they depend on are not valid, nothing will crash and a high-level rule will not be applied.
|
||||
|
||||
Example:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:age).filled(:integer)
|
||||
end
|
||||
|
||||
result = schema.call(email: 'jane@doe.org', age: 21)
|
||||
|
||||
# access validation output data
|
||||
result.to_h
|
||||
# => {:email=>'jane@doe.org', :age=>21}
|
||||
|
||||
# check if all rules passed
|
||||
result.success?
|
||||
# => true
|
||||
|
||||
# check if any of the rules failed
|
||||
result.failure?
|
||||
# => false
|
||||
```
|
||||
|
||||
### Defining base schema class
|
||||
|
||||
```ruby
|
||||
class AppSchema < Dry::Schema::Params
|
||||
config.messages.load_paths << '/my/app/config/locales/en.yml'
|
||||
config.messages.backend = :i18n
|
||||
|
||||
define do
|
||||
# define common rules, if any
|
||||
end
|
||||
end
|
||||
|
||||
# now you can build other schemas on top of the base one:
|
||||
class MySchema < AppSchema
|
||||
# define your rules
|
||||
end
|
||||
|
||||
my_schema = MySchema.new
|
||||
```
|
||||
|
||||
### Working with error messages
|
||||
|
||||
The result object returned by `Schema#call` provides an API to convert error objects to human-friendly messages.
|
||||
|
||||
```ruby
|
||||
result = schema.call(email: nil, age: 21)
|
||||
|
||||
# get default errors
|
||||
result.errors.to_h
|
||||
# => {:email=>['must be filled']}
|
||||
|
||||
# get full errors
|
||||
result.errors(full: true).to_h
|
||||
# => {:email=>['email must be filled']}
|
||||
|
||||
# get errors in another language
|
||||
result.errors(locale: :pl).to_h
|
||||
# => {:email=>['musi być wypełniony']}
|
||||
```
|
||||
|
||||
### Checking presence of errors
|
||||
|
||||
You can ask result object if there are any errors under given path.
|
||||
|
||||
``` ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:name).filled(:string)
|
||||
optional(:tags).array(:str?)
|
||||
end
|
||||
|
||||
result = schema.call(name: "", tags: ["red", 123])
|
||||
|
||||
result.error?(:name)
|
||||
# => true
|
||||
|
||||
result.error?(:tags)
|
||||
# => true
|
||||
|
||||
result.error?([:tags, 0])
|
||||
# => false
|
||||
|
||||
result.error?([:tags, 1])
|
||||
# => true
|
||||
```
|
||||
|
||||
### Learn more
|
||||
|
||||
- [Customizing messages](/gems/dry-schema/error-messages)
|
||||
- [Validation hints](/gems/dry-schema/extensions/hints)
|
|
@ -0,0 +1,136 @@
|
|||
---
|
||||
title: Error messages
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
By default `dry-schema` comes with a set of pre-defined error messages for every built-in predicate. They are defined in [a yaml file](https://github.com/dry-rb/dry-schema/blob/master/config/errors.yml) which is shipped with the gem. This file is compatible with `I18n` format.
|
||||
|
||||
## Configuration
|
||||
|
||||
You can provide your own messages and configure your schemas to use it like that:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
config.messages.load_paths << '/path/to/my/errors.yml'
|
||||
end
|
||||
```
|
||||
|
||||
You can also provide a namespace per-schema that will be used by default:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
config.messages.namespace = :user
|
||||
end
|
||||
```
|
||||
|
||||
You can change the default top namespace using:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
config.messages.top_namespace = :validation_schema
|
||||
end
|
||||
```
|
||||
|
||||
## Lookup rules
|
||||
|
||||
```yaml
|
||||
en:
|
||||
dry_schema:
|
||||
errors:
|
||||
size?:
|
||||
arg:
|
||||
default: "size must be %{num}"
|
||||
range: "size must be within %{left} - %{right}"
|
||||
|
||||
value:
|
||||
string:
|
||||
arg:
|
||||
default: "length must be %{num}"
|
||||
range: "length must be within %{left} - %{right}"
|
||||
|
||||
filled?: "must be filled"
|
||||
|
||||
included_in?: "must be one of %{list}"
|
||||
excluded_from?: "must not be one of: %{list}"
|
||||
|
||||
rules:
|
||||
email:
|
||||
filled?: "the email is missing"
|
||||
|
||||
user:
|
||||
filled?: "name cannot be blank"
|
||||
|
||||
rules:
|
||||
address:
|
||||
filled?: "You gotta tell us where you live"
|
||||
```
|
||||
|
||||
Given the yaml file above, messages lookup works as follows:
|
||||
|
||||
```ruby
|
||||
messages = Dry::Schema::Messages::YAML.load(%w(/path/to/our/errors.yml))
|
||||
|
||||
# matching arg type for size? predicate
|
||||
messages[:size?, rule: :name, arg_type: Fixnum] # => "size must be %{num}"
|
||||
messages[:size?, rule: :name, arg_type: Range] # => "size must be within %{left} - %{right}"
|
||||
|
||||
# matching val type for size? predicate
|
||||
messages[:size?, rule: :name, val_type: String] # => "length must be %{num}"
|
||||
|
||||
# matching predicate
|
||||
messages[:filled?, rule: :age] # => "must be filled"
|
||||
messages[:filled?, rule: :address] # => "must be filled"
|
||||
|
||||
# matching predicate for a specific rule
|
||||
messages[:filled?, rule: :email] # => "the email is missing"
|
||||
|
||||
# with namespaced messages
|
||||
user_messages = messages.namespaced(:user)
|
||||
|
||||
user_messages[:filled?, rule: :age] # "cannot be blank"
|
||||
user_messages[:filled?, rule: :address] # "You gotta tell us where you live"
|
||||
```
|
||||
|
||||
By configuring `load_paths` and/or `namespace` in a schema, default messages are going to be automatically merged with your overrides and/or namespaced.
|
||||
|
||||
### I18n integration
|
||||
|
||||
If you are using `i18n` gem and load it before `dry-schema` then you'll be able to configure a schema to use `i18n` messages:
|
||||
|
||||
```ruby
|
||||
require 'i18n'
|
||||
require 'dry-schema'
|
||||
|
||||
schema = Dry::Schema.Params do
|
||||
config.messages.backend = :i18n
|
||||
|
||||
required(:email).filled(:string)
|
||||
end
|
||||
|
||||
# return default translations
|
||||
schema.call(email: '').errors.to_h
|
||||
{ :email => ["must be filled"] }
|
||||
|
||||
# return other translations (assuming you have it :))
|
||||
puts schema.call(email: '').errors(locale: :pl).to_h
|
||||
{ :email => ["musi być wypełniony"] }
|
||||
```
|
||||
|
||||
Important: I18n must be initialized before using a schema, `dry-schema` does not try to do it for you, it only sets its default error translations automatically.
|
||||
|
||||
### Full messages
|
||||
|
||||
By default, messages do not include a rule's name, if you want it to be included simply use `:full` option:
|
||||
|
||||
```ruby
|
||||
schema.call(email: '').errors(full: true).to_h
|
||||
{ :email => ["email must be filled"] }
|
||||
```
|
||||
|
||||
### Finding the right key
|
||||
|
||||
`dry-schema` has one error key for each kind of validation (Refer to [`errors.yml`](https://github.com/dry-rb/dry-schema/blob/master/config/errors.yml) for the full list). `key?` and `filled?` can usually be mistaken for each other, so pay attention to them:
|
||||
|
||||
- `key?`: a required parameter is missing in the `params` hash.
|
||||
- `filled?`: a required parameter is in the `params` hash but has an empty value.
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Extensions
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
sections:
|
||||
- hints
|
||||
- monads
|
||||
---
|
||||
|
||||
`dry-schema` can be extended with extension. Those extensions are loaded with `Dry::Schema.load_extensions`.
|
||||
|
||||
Available extensions:
|
||||
|
||||
- [Hints](/gems/dry-schema/extensions/hints)
|
||||
- [Monads](/gems/dry-schema/extensions/monads)
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: Hints
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
In addition to error messages, you can also access hints, which are generated from your rules. While `errors` tell you which predicate checks failed, `hints` tell you which additional predicate checks *were not evaluated* because an earlier predicate failed:
|
||||
|
||||
```ruby
|
||||
# enable :hints
|
||||
|
||||
Dry::Schema.load_extensions(:hints)
|
||||
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
result = schema.call(email: 'jane@doe.org', age: '')
|
||||
result.hints.to_h
|
||||
|
||||
# {:age=>['must be greater than 18']}
|
||||
|
||||
result = schema.call(email: 'jane@doe.org', age: '')
|
||||
|
||||
result.errors.to_h
|
||||
# {:age=>['must be filled']}
|
||||
|
||||
result.hints.to_h
|
||||
# {:age=>['must be greater than 18']}
|
||||
# hints takes the same options as errors:
|
||||
|
||||
result.hints(full: true)
|
||||
# {:age=>['age must be greater than 18']}
|
||||
```
|
||||
|
||||
You can also use `messages` to get a combination of both errors and hints:
|
||||
|
||||
```ruby
|
||||
result = schema.call(email: 'jane@doe.org', age: '')
|
||||
result.messages.to_h
|
||||
# {:age=>["must be filled", "must be greater than 18"]}
|
||||
```
|
||||
|
||||
### Learn more
|
||||
|
||||
- [Customizing messages](/gems/dry-schema/error-messages)
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: Monads
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
The monads extension makes `Dry::Schema::Result` objects compatible with `dry-monads`.
|
||||
|
||||
To enable the extension:
|
||||
|
||||
```ruby
|
||||
require 'dry/schema'
|
||||
|
||||
Dry::Schema.load_extensions(:monads)
|
||||
```
|
||||
|
||||
After loading the extension, you can leverage monad API:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params { required(:name).filled(:str?, size?: 2..4) }
|
||||
|
||||
schema.call(name: 'Jane').to_monad # => Dry::Monads::Success(#<Dry::Schema::Result{:name=>"Jane"} errors={}>)
|
||||
|
||||
schema.call(name: '').to_monad # => Dry::Monads::Failure(#<Dry::Schema::Result{:name=>""} errors={:name=>["must be filled"]}>)
|
||||
|
||||
schema.(name: "")
|
||||
.to_monad
|
||||
.fmap { |r| puts "passed: #{r.to_h.inspect}" }
|
||||
.or { |r| puts "failed: #{r.errors.to_h.inspect}" }
|
||||
```
|
||||
|
||||
This can be useful when used with `dry-monads` and the [`do` notation](/gems/dry-monads/do-notation/).
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
title: Introduction
|
||||
description: Schema coercion & validation
|
||||
layout: gem-single
|
||||
type: gem
|
||||
name: dry-schema
|
||||
sections:
|
||||
- basics
|
||||
- optional-keys-and-values
|
||||
- nested-data
|
||||
- reusing-schemas
|
||||
- params
|
||||
- json
|
||||
- error-messages
|
||||
- advanced
|
||||
- extensions
|
||||
---
|
||||
|
||||
`dry-schema` is a validation library for **data structures**. It ships with a set of many built-in predicates and powerful macros that allow you to define even complex validation logic with very concise syntax.
|
||||
|
||||
Main focus of this library is on:
|
||||
|
||||
- Data **structure** validation
|
||||
- Value **types** validation
|
||||
|
||||
^INFO
|
||||
`dry-schema` is also used as the schema engine in [dry-validation](/gems/dry-validation)
|
||||
^
|
||||
|
||||
### Unique features
|
||||
|
||||
There are a few features of `dry-schema` that make it unique:
|
||||
|
||||
* [Structural validation](/gems/dry-schema/optional-keys-and-values) where key presence can be verified separately from values. This removes ambiguity related to "presence" validation where you don't know if value is indeed `nil` or if a key is missing in the input hash
|
||||
* [Pre-coercion validation using filtering rules](/gems/dry-schema/advanced/filtering)
|
||||
* Explicit coercion logic - rather than implementing complex generic coercions, `dry-schema` uses coercion types from `dry-types` which are faster and more strict than generic coercions
|
||||
* Support for [validating array elements](/gems/dry-schema/basics/macros#array) with convenient access to error messages
|
||||
* Powerful introspection - you have access to [key maps](/gems/dry-schema/advanced/key-maps) and detailed [Rule AST](/gems/dry-schema/advanced/rule-ast)
|
||||
* Performance - multiple times faster than validations based on `ActiveModel` and `strong parameters`
|
||||
* Configurable, localized error messages with or *without* `I18n` gem
|
||||
|
||||
### When to use?
|
||||
|
||||
Always and everywhere. This is a general-purpose data validation library that can be used for many things and **it's multiple times faster** than `ActiveRecord`/`ActiveModel::Validations` _and_ `strong-parameters`.
|
||||
|
||||
Possible use-cases include validation of:
|
||||
|
||||
- Form params
|
||||
- "GET" params
|
||||
- JSON documents
|
||||
- YAML documents
|
||||
- Application configuration (ie stored in ENV)
|
||||
- Replacement for `strong-parameters`
|
||||
- etc.
|
||||
|
||||
### Quick start
|
||||
|
||||
```ruby
|
||||
require 'dry/schema'
|
||||
|
||||
UserSchema = Dry::Schema.Params do
|
||||
required(:name).filled(:string)
|
||||
required(:email).filled(:string)
|
||||
|
||||
required(:age).maybe(:integer)
|
||||
|
||||
required(:address).hash do
|
||||
required(:street).filled(:string)
|
||||
required(:city).filled(:string)
|
||||
required(:zipcode).filled(:string)
|
||||
end
|
||||
end
|
||||
|
||||
UserSchema.(
|
||||
name: 'Jane',
|
||||
email: 'jane@doe.org',
|
||||
address: { street: 'Street 1', city: 'NYC', zipcode: '1234' }
|
||||
).inspect
|
||||
|
||||
# #<Dry::Schema::Result{:name=>"Jane", :email=>"jane@doe.org", :address=>{:street=>"Street 1", :city=>"NYC", :zipcode=>"1234"}} errors={:age=>["age is missing"]}>
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: JSON
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
To validate JSON data structures, you can use `JSON` schemas. The difference between `Params` and `JSON` is coercion logic. Refer to [dry-types](/gems/dry-types/built-in-types/) documentation for more information about supported JSON coercions.
|
||||
|
||||
### Examples
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.JSON do
|
||||
required(:email).filled(:string)
|
||||
|
||||
required(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
errors = schema.call('email' => '', 'age' => '18').errors.to_h
|
||||
|
||||
puts errors.inspect
|
||||
# {
|
||||
# :email => ["must be filled"],
|
||||
# :age => ["must be greater than 18"]
|
||||
# }
|
||||
```
|
||||
|
||||
> **Notice** that JSON schemas are suitable for checking hash objects *exclusively*. There's an outstanding [issue](https://github.com/dry-rb/dry-schema/issues/23)
|
||||
> about making it work with any JSON-compatible input.
|
|
@ -0,0 +1,127 @@
|
|||
---
|
||||
title: Nested data
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
`dry-schema` supports validation of nested data.
|
||||
|
||||
### Nested `Hash`
|
||||
|
||||
To define validation rules for a nested hash you can use the same DSL on a specific key:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:address).hash do
|
||||
required(:city).filled(:string, min_size?: 3)
|
||||
required(:street).filled(:string)
|
||||
required(:country).hash do
|
||||
required(:name).filled(:string)
|
||||
required(:code).filled(:string)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
errors = schema.call({}).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# { :address => ["is missing"] }
|
||||
|
||||
errors = schema.call(address: { city: 'NYC' }).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {
|
||||
# :address => [
|
||||
# { :street => ["is missing"] },
|
||||
# { :country => ["is missing"] }
|
||||
# ]
|
||||
# }
|
||||
```
|
||||
|
||||
### Nested Maybe `Hash`
|
||||
|
||||
If a nested hash could be `nil`, simply use `maybe` macro with a block:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:address).maybe do
|
||||
hash do
|
||||
required(:city).filled(:string, min_size?: 3)
|
||||
required(:street).filled(:string)
|
||||
required(:country).hash do
|
||||
required(:name).filled(:string)
|
||||
required(:code).filled(:string)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
schema.(address: nil).success? # true
|
||||
```
|
||||
|
||||
### Nested `Array`
|
||||
|
||||
You can use the `array` macro for validating each element in an array:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:phone_numbers).array(:str?)
|
||||
end
|
||||
|
||||
errors = schema.call(phone_numbers: '').messages
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# { :phone_numbers => ["must be an array"] }
|
||||
|
||||
errors = schema.call(phone_numbers: ['123456789', 123456789]).messages
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {
|
||||
# :phone_numbers => {
|
||||
# 1 => ["must be a string"]
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
You can use `array(:hash)` and `schema` to validate an array of hashes:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:people).array(:hash) do
|
||||
required(:name).filled(:string)
|
||||
required(:age).filled(:integer, gteq?: 18)
|
||||
end
|
||||
end
|
||||
|
||||
errors = schema.call(people: [{ name: 'Alice', age: 19 }, { name: 'Bob', age: 17 }]).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# => {
|
||||
# :people=>{
|
||||
# 1=>{
|
||||
# :age=>["must be greater than or equal to 18"]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
```
|
||||
|
||||
To add array predicates, use the full form:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:people).value(:array, min_size?: 1).each do
|
||||
hash do
|
||||
required(:name).filled(:string)
|
||||
required(:age).filled(:integer, gteq?: 18)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
errors = schema.call(people: []).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# => {
|
||||
# :people=>["size cannot be less than 1"]
|
||||
# }
|
||||
|
||||
```
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
title: Optional keys and values
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
We make a clear distinction between specifying an optional **key** and an optional **value**. This gives you a way of being very specific about validation rules. You can define a schema which gives you precise errors when a key is missing or key is present but the value is `nil`.
|
||||
|
||||
This also comes with the benefit of being explicit about the type expectation. In the example below we explicitly state that `:age` _can be omitted_ or if present it _must be an integer_ and it _must be greater than 18_.
|
||||
|
||||
### Optional keys
|
||||
|
||||
You can define which keys are optional and define rules for their values:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
optional(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
errors = schema.call(email: 'jane@doe.org').errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {}
|
||||
|
||||
errors = schema.call(email: 'jane@doe.org', age: 17).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# { :age => ["must be greater than 18"] }
|
||||
```
|
||||
|
||||
### Optional values
|
||||
|
||||
When it is allowed for a given value to be `nil` you can use `maybe` macro:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
optional(:age).maybe(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
errors = schema.call(email: 'jane@doe.org', age: nil).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {}
|
||||
|
||||
errors = schema.call(email: 'jane@doe.org', age: 19).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {}
|
||||
|
||||
errors = schema.call(email: 'jane@doe.org', age: 17).errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# { :age => ["must be greater than 18"] }
|
||||
```
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: Params
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
Probably the most common use case is to validate HTTP params. This is a special kind of a validation for a couple of reasons:
|
||||
|
||||
- The input is a hash with stringified keys
|
||||
- The input can include values that are strings, hashes or arrays
|
||||
- Prior to validation, we need to symbolize keys and coerce values based on the information in a schema
|
||||
|
||||
For that reason, `dry-schema` ships with `Params` schemas:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:age).filled(:integer, gt?: 18)
|
||||
end
|
||||
|
||||
errors = schema.call('email' => '', 'age' => '18').errors
|
||||
|
||||
puts errors.to_h.inspect
|
||||
# {
|
||||
# :email => ["must be filled"],
|
||||
# :age => ["must be greater than 18"]
|
||||
# }
|
||||
```
|
||||
|
||||
> Params-specific value coercion is handled by the hash type from `dry-types`. It is built automatically for you based on the type specs and used prior to applying the validation rules
|
||||
|
||||
### Handling empty strings
|
||||
|
||||
Your schema will automatically coerce empty strings to `nil` or an empty array, provided that you allow a value to be nil:
|
||||
|
||||
```ruby
|
||||
schema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:age).maybe(:integer, gt?: 18)
|
||||
required(:tags).maybe(:array)
|
||||
end
|
||||
|
||||
result = schema.call('email' => 'jane@doe.org', 'age' => '', 'tags' => '')
|
||||
|
||||
puts result.to_h
|
||||
# {:email=>'jane@doe.org', :age=>nil, :tags=>[]}
|
||||
```
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
title: Reusing schemas
|
||||
layout: gem-single
|
||||
name: dry-schema
|
||||
---
|
||||
|
||||
You can easily reuse existing schemas using nested-schema syntax:
|
||||
|
||||
```ruby
|
||||
AddressSchema = Dry::Schema.Params do
|
||||
required(:street).filled(:string)
|
||||
required(:city).filled(:string)
|
||||
required(:zipcode).filled(:string)
|
||||
end
|
||||
|
||||
UserSchema = Dry::Schema.Params do
|
||||
required(:email).filled(:string)
|
||||
required(:name).filled(:string)
|
||||
required(:address).hash(AddressSchema)
|
||||
end
|
||||
|
||||
UserSchema.(
|
||||
email: 'jane@doe',
|
||||
name: 'Jane',
|
||||
address: { street: nil, city: 'NYC', zipcode: '123' }
|
||||
).errors.to_h
|
||||
|
||||
# {:address=>{:street=>["must be filled"]}}
|
||||
```
|
Loading…
Reference in New Issue