dry-types 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.codeclimate.yml +2 -5
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +76 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +1 -1
- data/.rspec +3 -1
- data/.rubocop.yml +89 -0
- data/CHANGELOG.md +127 -3
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +2 -2
- data/Gemfile +12 -6
- data/LICENSE +17 -17
- data/README.md +2 -2
- data/Rakefile +2 -2
- data/benchmarks/hash_schemas.rb +8 -6
- data/benchmarks/lax_schema.rb +0 -1
- data/benchmarks/profile_invalid_input.rb +1 -1
- data/benchmarks/profile_lax_schema_valid.rb +1 -1
- data/benchmarks/profile_valid_input.rb +1 -1
- data/docsite/source/array-with-member.html.md +13 -0
- data/docsite/source/built-in-types.html.md +116 -0
- data/docsite/source/constraints.html.md +31 -0
- data/docsite/source/custom-types.html.md +93 -0
- data/docsite/source/default-values.html.md +91 -0
- data/docsite/source/enum.html.md +69 -0
- data/docsite/source/extensions.html.md +15 -0
- data/docsite/source/extensions/maybe.html.md +57 -0
- data/docsite/source/extensions/monads.html.md +61 -0
- data/docsite/source/getting-started.html.md +57 -0
- data/docsite/source/hash-schemas.html.md +169 -0
- data/docsite/source/index.html.md +156 -0
- data/docsite/source/map.html.md +17 -0
- data/docsite/source/optional-values.html.md +35 -0
- data/docsite/source/sum.html.md +21 -0
- data/dry-types.gemspec +19 -19
- data/lib/dry/types.rb +9 -4
- data/lib/dry/types/any.rb +2 -2
- data/lib/dry/types/array.rb +6 -0
- data/lib/dry/types/array/constructor.rb +32 -0
- data/lib/dry/types/array/member.rb +10 -3
- data/lib/dry/types/builder.rb +2 -2
- data/lib/dry/types/builder_methods.rb +34 -8
- data/lib/dry/types/coercions.rb +19 -6
- data/lib/dry/types/coercions/params.rb +4 -4
- data/lib/dry/types/compiler.rb +2 -2
- data/lib/dry/types/constrained.rb +6 -1
- data/lib/dry/types/constructor.rb +10 -42
- data/lib/dry/types/constructor/function.rb +4 -5
- data/lib/dry/types/core.rb +27 -8
- data/lib/dry/types/decorator.rb +3 -2
- data/lib/dry/types/enum.rb +2 -1
- data/lib/dry/types/extensions.rb +4 -0
- data/lib/dry/types/extensions/maybe.rb +9 -1
- data/lib/dry/types/extensions/monads.rb +29 -0
- data/lib/dry/types/hash.rb +11 -12
- data/lib/dry/types/hash/constructor.rb +5 -5
- data/lib/dry/types/json.rb +4 -0
- data/lib/dry/types/lax.rb +4 -4
- data/lib/dry/types/map.rb +8 -4
- data/lib/dry/types/meta.rb +1 -1
- data/lib/dry/types/module.rb +6 -6
- data/lib/dry/types/nominal.rb +3 -4
- data/lib/dry/types/params.rb +9 -0
- data/lib/dry/types/predicate_inferrer.rb +197 -0
- data/lib/dry/types/predicate_registry.rb +34 -0
- data/lib/dry/types/primitive_inferrer.rb +97 -0
- data/lib/dry/types/printer.rb +17 -12
- data/lib/dry/types/schema.rb +16 -22
- data/lib/dry/types/schema/key.rb +19 -1
- data/lib/dry/types/spec/types.rb +6 -7
- data/lib/dry/types/sum.rb +2 -2
- data/lib/dry/types/version.rb +1 -1
- metadata +67 -35
- data/.travis.yml +0 -27
@@ -0,0 +1,91 @@
|
|
1
|
+
---
|
2
|
+
title: Default Values
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
A type with a default value will return the configured value when the input is not defined:
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
PostStatus = Types::String.default('draft')
|
11
|
+
|
12
|
+
PostStatus[] # "draft"
|
13
|
+
PostStatus["published"] # "published"
|
14
|
+
PostStatus[true] # raises ConstraintError
|
15
|
+
```
|
16
|
+
|
17
|
+
It works with a callable value:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
CallableDateTime = Types::DateTime.default { DateTime.now }
|
21
|
+
|
22
|
+
CallableDateTime[]
|
23
|
+
# => #<DateTime: 2017-05-06T00:43:06+03:00 ((2457879j,78186s,649279000n),+10800s,2299161j)>
|
24
|
+
CallableDateTime[]
|
25
|
+
# => #<DateTime: 2017-05-06T00:43:07+03:00 ((2457879j,78187s,635494000n),+10800s,2299161j)>
|
26
|
+
```
|
27
|
+
|
28
|
+
`Dry::Types::Undefined` can be passed explicitly as a missing value:
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
PostStatus = Types::String.default('draft')
|
32
|
+
|
33
|
+
PostStatus[Dry::Types::Undefined] # "draft"
|
34
|
+
```
|
35
|
+
|
36
|
+
It also receives the type constructor as an argument:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
CallableDateTime = Types::DateTime.constructor(&:to_datetime).default { |type| type[Time.now] }
|
40
|
+
|
41
|
+
CallableDateTime[Time.now]
|
42
|
+
# => #<DateTime: 2017-05-06T01:13:06+03:00 ((2457879j,79986s,63464000n),+10800s,2299161j)>
|
43
|
+
CallableDateTime[Date.today]
|
44
|
+
# => #<DateTime: 2017-05-06T00:00:00+00:00 ((2457880j,0s,0n),+0s,2299161j)>
|
45
|
+
CallableDateTime[]
|
46
|
+
# => #<DateTime: 2017-05-06T01:13:06+03:00 ((2457879j,79986s,63503000n),+10800s,2299161j)>
|
47
|
+
```
|
48
|
+
|
49
|
+
**Be careful:** types will return the **same instance** of the default value every time. This may cause problems if you mutate the returned value after receiving it:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
default_0 = PostStatus.()
|
53
|
+
# => "draft"
|
54
|
+
default_1 = PostStatus.()
|
55
|
+
# => "draft"
|
56
|
+
|
57
|
+
# Both variables point to the same string:
|
58
|
+
default_0.object_id == default_1.object_id
|
59
|
+
# => true
|
60
|
+
|
61
|
+
# Mutating the string will change the default value of type:
|
62
|
+
default_0 << '_mutated'
|
63
|
+
PostStatus.(nil)
|
64
|
+
# => "draft_mutated" # not "draft"
|
65
|
+
```
|
66
|
+
|
67
|
+
You can guard against these kind of errors by calling `freeze` when setting the default:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
PostStatus = Types::Params::String.default('draft'.freeze)
|
71
|
+
default = PostStatus.()
|
72
|
+
default << 'attempt to mutate default'
|
73
|
+
# => RuntimeError: can't modify frozen string
|
74
|
+
|
75
|
+
# If you really want to mutate it, call `dup` on it first:
|
76
|
+
default = default.dup
|
77
|
+
default << "this time it'll work"
|
78
|
+
```
|
79
|
+
|
80
|
+
**Warning on using with constrained types**: If the value passed to the `.default` block does not match the type constraints, this will not throw an exception, because it is not passed to the constructor and will be used as is.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
CallableDateTime = Types::DateTime.constructor(&:to_datetime).default { Time.now }
|
84
|
+
|
85
|
+
CallableDateTime[Time.now]
|
86
|
+
# => #<DateTime: 2017-05-06T00:50:09+03:00 ((2457879j,78609s,839588000n),+10800s,2299161j)>
|
87
|
+
CallableDateTime[Date.today]
|
88
|
+
# => #<DateTime: 2017-05-06T00:00:00+00:00 ((2457880j,0s,0n),+0s,2299161j)>
|
89
|
+
CallableDateTime[]
|
90
|
+
# => 2017-05-06 00:50:15 +0300
|
91
|
+
```
|
@@ -0,0 +1,69 @@
|
|
1
|
+
---
|
2
|
+
title: Enum
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
In many cases you may want to define an enum. For example, in a blog application a post may have a finite list of statuses. Apart from accessing the current status value, it is useful to have all possible values accessible too. Furthermore, an enum can be a map from, e.g., strings to integers. This is useful for mapping externally-provided integer values to human-readable strings without explicit conversions, see examples.
|
8
|
+
|
9
|
+
``` ruby
|
10
|
+
require 'dry-types'
|
11
|
+
require 'dry-struct'
|
12
|
+
|
13
|
+
module Types
|
14
|
+
include Dry.Types()
|
15
|
+
end
|
16
|
+
|
17
|
+
class Post < Dry::Struct
|
18
|
+
Statuses = Types::String.enum('draft', 'published', 'archived')
|
19
|
+
|
20
|
+
attribute :title, Types::String
|
21
|
+
attribute :body, Types::String
|
22
|
+
attribute :status, Statuses
|
23
|
+
end
|
24
|
+
|
25
|
+
# enum values are frozen, let's be paranoid, doesn't hurt and have potential to
|
26
|
+
# eliminate silly bugs
|
27
|
+
Post::Statuses.values.frozen? # => true
|
28
|
+
Post::Statuses.values.all?(&:frozen?) # => true
|
29
|
+
|
30
|
+
Post::Statuses['draft'] # => "draft"
|
31
|
+
|
32
|
+
# it'll raise if something silly was passed in
|
33
|
+
Post::Statuses['something silly']
|
34
|
+
# => Dry::Types::ConstraintError: "something silly" violates constraints
|
35
|
+
|
36
|
+
# nil is considered as something silly too
|
37
|
+
Post::Statuses[nil]
|
38
|
+
# => Dry::Types::ConstraintError: nil violates constraints
|
39
|
+
```
|
40
|
+
|
41
|
+
Note that if you want to define an enum type with a default, you must call `.default` *before* calling `.enum`, not the other way around:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# this is the correct usage:
|
45
|
+
Dry::Types::String.default('red').enum('blue', 'green', 'red')
|
46
|
+
|
47
|
+
# this will raise an error:
|
48
|
+
Dry::Types::String.enum('blue', 'green', 'red').default('red')
|
49
|
+
```
|
50
|
+
|
51
|
+
### Mappings
|
52
|
+
|
53
|
+
A classic example is mapping integers coming from somewhere (API/database/etc) to something more understandable:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class Cell < Dry::Struct
|
57
|
+
attribute :state, Types::String.enum('locked' => 0, 'open' => 1)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
Cell.new(state: 'locked')
|
62
|
+
# => #<Cell state="locked">
|
63
|
+
|
64
|
+
# Integers are accepted too
|
65
|
+
Cell.new(state: 0)
|
66
|
+
# => #<Cell state="locked">
|
67
|
+
Cell.new(state: 1)
|
68
|
+
# => #<Cell state="open">
|
69
|
+
```
|
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
title: Extensions
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
sections:
|
6
|
+
- maybe
|
7
|
+
- monads
|
8
|
+
---
|
9
|
+
|
10
|
+
`dry-types` can be extended with extension. Those extensions are loaded with `Dry::Types.load_extensions`.
|
11
|
+
|
12
|
+
Available extensions:
|
13
|
+
|
14
|
+
- [Maybe](docs::extensions/maybe)
|
15
|
+
- [Monads](docs::extensions/monads)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
---
|
2
|
+
title: Maybe
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
The [dry-monads gem](/gems/dry-monads/) provides approach to handling optional values by returning a [_Monad_](/gems/dry-monads/) object. This allows you to pass your type to a `Maybe(x)` block that only executes if `x` returns `Some` or `None`.
|
8
|
+
|
9
|
+
> NOTE: Requires the [dry-monads gem](/gems/dry-monads/) to be loaded.
|
10
|
+
1. Load the `:maybe` extension in your application.
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
require 'dry-types'
|
14
|
+
|
15
|
+
Dry::Types.load_extensions(:maybe)
|
16
|
+
module Types
|
17
|
+
include Dry.Types()
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
2. Append `.maybe` to a _Type_ to return a _Monad_ object
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
x = Types::Maybe::Strict::Integer[nil]
|
25
|
+
Maybe(x) { puts(x) }
|
26
|
+
x = Types::Maybe::Coercible::String[nil]
|
27
|
+
Maybe(x) { puts(x) }
|
28
|
+
x = Types::Maybe::Strict::Integer[123]
|
29
|
+
Maybe(x) { puts(x) }
|
30
|
+
x = Types::Maybe::Strict::String[123]
|
31
|
+
Maybe(x) { puts(x) }
|
32
|
+
```
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
Types::Maybe::Strict::Integer[nil] # None
|
36
|
+
Types::Maybe::Strict::Integer[123] # Some(123)
|
37
|
+
Types::Maybe::Coercible::Float[nil] # None
|
38
|
+
Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
|
39
|
+
# 'Maybe' types can also accessed by calling '.maybe' on a regular type:
|
40
|
+
Types::Strict::Integer.maybe # equivalent to Types::Maybe::Strict::Integer
|
41
|
+
```
|
42
|
+
|
43
|
+
You can define your own optional types:
|
44
|
+
|
45
|
+
``` ruby
|
46
|
+
maybe_string = Types::Strict::String.maybe
|
47
|
+
maybe_string[nil]
|
48
|
+
# => None
|
49
|
+
maybe_string[nil].fmap(&:upcase)
|
50
|
+
# => None
|
51
|
+
maybe_string['something']
|
52
|
+
# => Some('something')
|
53
|
+
maybe_string['something'].fmap(&:upcase)
|
54
|
+
# => Some('SOMETHING')
|
55
|
+
maybe_string['something'].fmap(&:upcase).value_or('NOTHING')
|
56
|
+
# => "SOMETHING"
|
57
|
+
```
|
@@ -0,0 +1,61 @@
|
|
1
|
+
---
|
2
|
+
title: Monads
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
The monads extension makes `Dry::Types::Result` objects compatible with `dry-monads`.
|
8
|
+
|
9
|
+
To enable the extension:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'dry/types'
|
13
|
+
Dry::Types.load_extensions(:monads)
|
14
|
+
```
|
15
|
+
|
16
|
+
After loading the extension, you can leverage monad API:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
Types = Dry.Types()
|
20
|
+
|
21
|
+
result = Types::String.try('Jane')
|
22
|
+
result.class #=> Dry::Types::Result::Success
|
23
|
+
monad = result.to_monad
|
24
|
+
monad.class #=> Dry::Monads::Result::Success
|
25
|
+
monad.value! # => 'Jane'
|
26
|
+
result = Types::String.try(nil)
|
27
|
+
result.class #=> Dry::Types::Result::Failure
|
28
|
+
monad = result.to_monad
|
29
|
+
monad.class #=> Dry::Monads::Result::Failure
|
30
|
+
monad.failure # => [#<Dry::Types::ConstraintError>, nil]
|
31
|
+
Types::String.try(nil)
|
32
|
+
.to_monad
|
33
|
+
.fmap { |result| puts "passed: #{result.inspect}" }
|
34
|
+
.or { |error, input| puts "input '#{input.inspect}' failed with error: #{error.to_s}" }
|
35
|
+
```
|
36
|
+
|
37
|
+
This can be useful when used with `dry-monads` and the [`do` notation](/gems/dry-monads/1.0/do-notation/):
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
require 'dry/types'
|
41
|
+
Types = Dry.Types()
|
42
|
+
Dry::Types.load_extensions(:monads)
|
43
|
+
|
44
|
+
class AddTen
|
45
|
+
include Dry::Monads[:result, :do]
|
46
|
+
|
47
|
+
def call(input)
|
48
|
+
integer = yield Types::Coercible::Integer.try(input)
|
49
|
+
|
50
|
+
Success(integer + 10)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
add_ten = AddTen.new
|
55
|
+
|
56
|
+
add_ten.call(10)
|
57
|
+
# => Success(20)
|
58
|
+
|
59
|
+
add_ten.call('integer')
|
60
|
+
# => Failure([#<Dry::Types::CoercionError: invalid value for Integer(): "integer">, "integer"])
|
61
|
+
```
|
@@ -0,0 +1,57 @@
|
|
1
|
+
---
|
2
|
+
title: Getting Started
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
### Using `Dry::Types` in Your Application
|
8
|
+
|
9
|
+
1. Make `Dry::Types` available to the application by creating a namespace that includes `Dry::Types`:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
module Types
|
13
|
+
include Dry.Types()
|
14
|
+
end
|
15
|
+
```
|
16
|
+
|
17
|
+
2. Reload the environment, & type `Types::Coercible::String` in the ruby console to confirm it worked:
|
18
|
+
|
19
|
+
``` ruby
|
20
|
+
Types::Coercible::String
|
21
|
+
# => #<Dry::Types::Constructor type=#<Dry::Types::Definition primitive=String options={}>>
|
22
|
+
```
|
23
|
+
|
24
|
+
### Creating Your First Type
|
25
|
+
|
26
|
+
1. Define a struct's types by passing the name & type to the `attribute` method:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class User < Dry::Struct
|
30
|
+
attribute :name, Types::String
|
31
|
+
end
|
32
|
+
```
|
33
|
+
|
34
|
+
2. Define [Custom Types](docs::custom-types) in the `Types` module, then pass the name & type to `attribute`:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
module Types
|
38
|
+
include Dry.Types()
|
39
|
+
|
40
|
+
Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
|
41
|
+
Age = Integer.constrained(gt: 18)
|
42
|
+
end
|
43
|
+
class User < Dry::Struct
|
44
|
+
attribute :name, Types::String
|
45
|
+
attribute :email, Types::Email
|
46
|
+
attribute :age, Types::Age
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
3. Use a `Dry::Struct` as a type:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class Message < Dry::Struct
|
54
|
+
attribute :body, Types::String
|
55
|
+
attribute :to, User
|
56
|
+
end
|
57
|
+
```
|
@@ -0,0 +1,169 @@
|
|
1
|
+
---
|
2
|
+
title: Hash Schemas
|
3
|
+
layout: gem-single
|
4
|
+
name: dry-types
|
5
|
+
---
|
6
|
+
|
7
|
+
It is possible to define a type for a hash with a known set of keys and corresponding value types. Let's say you want to describe a hash containing the name and the age of a user:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# using simple kernel coercions
|
11
|
+
user_hash = Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
|
12
|
+
|
13
|
+
user_hash[name: 'Jane', age: '21']
|
14
|
+
# => { name: 'Jane', age: 21 }
|
15
|
+
# :name left untouched and :age was coerced to Integer
|
16
|
+
```
|
17
|
+
|
18
|
+
If a value doesn't conform to the type, an error is raised:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
user_hash[name: :Jane, age: '21']
|
22
|
+
# => Dry::Types::SchemaError: :Jane (Symbol) has invalid type
|
23
|
+
# for :name violates constraints (type?(String, :Jane) failed)
|
24
|
+
```
|
25
|
+
|
26
|
+
All keys are required by default:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
user_hash[name: 'Jane']
|
30
|
+
# => Dry::Types::MissingKeyError: :age is missing in Hash input
|
31
|
+
```
|
32
|
+
|
33
|
+
Extra keys are omitted by default:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
user_hash[name: 'Jane', age: '21', city: 'London']
|
37
|
+
# => { name: 'Jane', age: 21 }
|
38
|
+
```
|
39
|
+
|
40
|
+
### Default values
|
41
|
+
|
42
|
+
Default types are **only** evaluated if the corresponding key is missing in the input:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
user_hash = Types::Hash.schema(
|
46
|
+
name: Types::String,
|
47
|
+
age: Types::Integer.default(18)
|
48
|
+
)
|
49
|
+
user_hash[name: 'Jane']
|
50
|
+
# => { name: 'Jane', age: 18 }
|
51
|
+
|
52
|
+
# nil violates the constraint
|
53
|
+
user_hash[name: 'Jane', age: nil]
|
54
|
+
# => Dry::Types::SchemaError: nil (NilClass) has invalid type
|
55
|
+
# for :age violates constraints (type?(Integer, nil) failed)
|
56
|
+
```
|
57
|
+
|
58
|
+
In order to evaluate default types on `nil`, wrap your type with a constructor and map `nil` to `Dry::Types::Undefined`:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
user_hash = Types::Hash.schema(
|
62
|
+
name: Types::String,
|
63
|
+
age: Types::Integer.
|
64
|
+
default(18).
|
65
|
+
constructor { |value|
|
66
|
+
value.nil? ? Dry::Types::Undefined : value
|
67
|
+
}
|
68
|
+
)
|
69
|
+
|
70
|
+
user_hash[name: 'Jane', age: nil]
|
71
|
+
# => { name: 'Jane', age: 18 }
|
72
|
+
```
|
73
|
+
|
74
|
+
The process of converting types to constructors like that can be automated, see "Type transformations" below.
|
75
|
+
|
76
|
+
### Optional keys
|
77
|
+
|
78
|
+
By default, all keys are required to present in the input. You can mark a key as optional by adding `?` to its name:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
user_hash = Types::Hash.schema(name: Types::String, age?: Types::Integer)
|
82
|
+
|
83
|
+
user_hash[name: 'Jane']
|
84
|
+
# => { name: 'Jane' }
|
85
|
+
```
|
86
|
+
|
87
|
+
### Extra keys
|
88
|
+
|
89
|
+
All keys not declared in the schema are silently ignored. This behavior can be changed by calling `.strict` on the schema:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
user_hash = Types::Hash.schema(name: Types::String).strict
|
93
|
+
user_hash[name: 'Jane', age: 21]
|
94
|
+
# => Dry::Types::UnknownKeysError: unexpected keys [:age] in Hash input
|
95
|
+
```
|
96
|
+
|
97
|
+
### Transforming input keys
|
98
|
+
|
99
|
+
Keys are supposed to be symbols but you can attach a key tranformation to a schema, e.g. for converting strings into symbols:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
user_hash = Types::Hash.schema(name: Types::String).with_key_transform(&:to_sym)
|
103
|
+
user_hash['name' => 'Jane']
|
104
|
+
|
105
|
+
# => { name: 'Jane' }
|
106
|
+
```
|
107
|
+
|
108
|
+
### Inheritance
|
109
|
+
|
110
|
+
Hash schemas can be inherited in a sense you can define a new schema based on an existing one. Declared keys will be merged, key and type transformations will be preserved. The `strict` option is also passed to the new schema if present.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Building an empty base schema
|
114
|
+
StrictSymbolizingHash = Types::Hash.schema({}).strict.with_key_transform(&:to_sym)
|
115
|
+
|
116
|
+
user_hash = StrictSymbolizingHash.schema(
|
117
|
+
name: Types::String
|
118
|
+
)
|
119
|
+
|
120
|
+
user_hash['name' => 'Jane']
|
121
|
+
# => { name: 'Jane' }
|
122
|
+
|
123
|
+
user_hash['name' => 'Jane', 'city' => 'London']
|
124
|
+
# => Dry::Types::UnknownKeysError: unexpected keys [:city] in Hash input
|
125
|
+
```
|
126
|
+
|
127
|
+
### Transforming types
|
128
|
+
|
129
|
+
A schema can transform types with a block. For example, the following code makes all keys optional:
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
user_hash = Types::Hash.with_type_transform { |type| type.required(false) }.schema(
|
133
|
+
name: Types::String,
|
134
|
+
age: Types::Integer
|
135
|
+
)
|
136
|
+
|
137
|
+
user_hash[name: 'Jane']
|
138
|
+
# => { name: 'Jane' }
|
139
|
+
user_hash[{}]
|
140
|
+
# => {}
|
141
|
+
```
|
142
|
+
|
143
|
+
Type transformations work perfectly with inheritance, you don't have to define same rules more than once:
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
SymbolizeAndOptionalSchema = Types::Hash.
|
147
|
+
.schema({})
|
148
|
+
.with_key_transform(&:to_sym)
|
149
|
+
.with_type_transform { |type| type.required(false) }
|
150
|
+
|
151
|
+
user_hash = SymbolizeAndOptionalSchema.schema(
|
152
|
+
name: Types::String,
|
153
|
+
age: Types::Integer
|
154
|
+
)
|
155
|
+
|
156
|
+
user_hash['name' => 'Jane']
|
157
|
+
```
|
158
|
+
|
159
|
+
You can check key name by calling `.name` on the type argument:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
Types::Hash.with_type_transform do |key|
|
163
|
+
if key.name.to_s.end_with?('_at')
|
164
|
+
key.constructor { |v| Time.iso8601(v) }
|
165
|
+
else
|
166
|
+
key
|
167
|
+
end
|
168
|
+
end
|
169
|
+
```
|