dry-types 1.0.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -5
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +76 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +1 -1
  10. data/.rspec +3 -1
  11. data/.rubocop.yml +89 -0
  12. data/CHANGELOG.md +127 -3
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +2 -2
  15. data/Gemfile +12 -6
  16. data/LICENSE +17 -17
  17. data/README.md +2 -2
  18. data/Rakefile +2 -2
  19. data/benchmarks/hash_schemas.rb +8 -6
  20. data/benchmarks/lax_schema.rb +0 -1
  21. data/benchmarks/profile_invalid_input.rb +1 -1
  22. data/benchmarks/profile_lax_schema_valid.rb +1 -1
  23. data/benchmarks/profile_valid_input.rb +1 -1
  24. data/docsite/source/array-with-member.html.md +13 -0
  25. data/docsite/source/built-in-types.html.md +116 -0
  26. data/docsite/source/constraints.html.md +31 -0
  27. data/docsite/source/custom-types.html.md +93 -0
  28. data/docsite/source/default-values.html.md +91 -0
  29. data/docsite/source/enum.html.md +69 -0
  30. data/docsite/source/extensions.html.md +15 -0
  31. data/docsite/source/extensions/maybe.html.md +57 -0
  32. data/docsite/source/extensions/monads.html.md +61 -0
  33. data/docsite/source/getting-started.html.md +57 -0
  34. data/docsite/source/hash-schemas.html.md +169 -0
  35. data/docsite/source/index.html.md +156 -0
  36. data/docsite/source/map.html.md +17 -0
  37. data/docsite/source/optional-values.html.md +35 -0
  38. data/docsite/source/sum.html.md +21 -0
  39. data/dry-types.gemspec +19 -19
  40. data/lib/dry/types.rb +9 -4
  41. data/lib/dry/types/any.rb +2 -2
  42. data/lib/dry/types/array.rb +6 -0
  43. data/lib/dry/types/array/constructor.rb +32 -0
  44. data/lib/dry/types/array/member.rb +10 -3
  45. data/lib/dry/types/builder.rb +2 -2
  46. data/lib/dry/types/builder_methods.rb +34 -8
  47. data/lib/dry/types/coercions.rb +19 -6
  48. data/lib/dry/types/coercions/params.rb +4 -4
  49. data/lib/dry/types/compiler.rb +2 -2
  50. data/lib/dry/types/constrained.rb +6 -1
  51. data/lib/dry/types/constructor.rb +10 -42
  52. data/lib/dry/types/constructor/function.rb +4 -5
  53. data/lib/dry/types/core.rb +27 -8
  54. data/lib/dry/types/decorator.rb +3 -2
  55. data/lib/dry/types/enum.rb +2 -1
  56. data/lib/dry/types/extensions.rb +4 -0
  57. data/lib/dry/types/extensions/maybe.rb +9 -1
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/hash.rb +11 -12
  60. data/lib/dry/types/hash/constructor.rb +5 -5
  61. data/lib/dry/types/json.rb +4 -0
  62. data/lib/dry/types/lax.rb +4 -4
  63. data/lib/dry/types/map.rb +8 -4
  64. data/lib/dry/types/meta.rb +1 -1
  65. data/lib/dry/types/module.rb +6 -6
  66. data/lib/dry/types/nominal.rb +3 -4
  67. data/lib/dry/types/params.rb +9 -0
  68. data/lib/dry/types/predicate_inferrer.rb +197 -0
  69. data/lib/dry/types/predicate_registry.rb +34 -0
  70. data/lib/dry/types/primitive_inferrer.rb +97 -0
  71. data/lib/dry/types/printer.rb +17 -12
  72. data/lib/dry/types/schema.rb +16 -22
  73. data/lib/dry/types/schema/key.rb +19 -1
  74. data/lib/dry/types/spec/types.rb +6 -7
  75. data/lib/dry/types/sum.rb +2 -2
  76. data/lib/dry/types/version.rb +1 -1
  77. metadata +67 -35
  78. 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
+ ```