Add docsite for version 1.0

This commit is contained in:
Piotr Solnica 2019-10-04 10:57:55 +02:00
parent 95e3feec31
commit 0263e85840
No known key found for this signature in database
GPG Key ID: 66BF2FDA7BA0F29C
21 changed files with 1711 additions and 0 deletions

View File

@ -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)

View File

@ -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`

View File

@ -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={}>
```

View File

@ -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)

View File

@ -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` => `^`

View File

@ -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}
# ]
```

View File

@ -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)

View File

@ -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
```

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

@ -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/).

View File

@ -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"]}>
```

View File

@ -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.

View File

@ -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"]
# }
```

View File

@ -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"] }
```

View File

@ -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=>[]}
```

View File

@ -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"]}}
```