dry-types 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.travis.yml +10 -4
  6. data/CHANGELOG.md +101 -3
  7. data/Gemfile +9 -6
  8. data/README.md +2 -2
  9. data/Rakefile +2 -2
  10. data/benchmarks/hash_schemas.rb +8 -6
  11. data/benchmarks/lax_schema.rb +0 -1
  12. data/benchmarks/profile_invalid_input.rb +1 -1
  13. data/benchmarks/profile_lax_schema_valid.rb +1 -1
  14. data/benchmarks/profile_valid_input.rb +1 -1
  15. data/docsite/source/array-with-member.html.md +13 -0
  16. data/docsite/source/built-in-types.html.md +116 -0
  17. data/docsite/source/constraints.html.md +31 -0
  18. data/docsite/source/custom-types.html.md +93 -0
  19. data/docsite/source/default-values.html.md +91 -0
  20. data/docsite/source/enum.html.md +69 -0
  21. data/docsite/source/getting-started.html.md +57 -0
  22. data/docsite/source/hash-schemas.html.md +169 -0
  23. data/docsite/source/index.html.md +155 -0
  24. data/docsite/source/map.html.md +17 -0
  25. data/docsite/source/optional-values.html.md +96 -0
  26. data/docsite/source/sum.html.md +21 -0
  27. data/dry-types.gemspec +19 -19
  28. data/lib/dry/types.rb +9 -4
  29. data/lib/dry/types/array.rb +6 -0
  30. data/lib/dry/types/array/constructor.rb +32 -0
  31. data/lib/dry/types/array/member.rb +8 -1
  32. data/lib/dry/types/builder.rb +1 -1
  33. data/lib/dry/types/builder_methods.rb +33 -23
  34. data/lib/dry/types/coercions.rb +19 -6
  35. data/lib/dry/types/coercions/params.rb +4 -4
  36. data/lib/dry/types/constrained.rb +5 -0
  37. data/lib/dry/types/constructor.rb +5 -37
  38. data/lib/dry/types/constructor/function.rb +4 -5
  39. data/lib/dry/types/core.rb +27 -8
  40. data/lib/dry/types/decorator.rb +1 -1
  41. data/lib/dry/types/enum.rb +1 -0
  42. data/lib/dry/types/extensions.rb +4 -0
  43. data/lib/dry/types/extensions/maybe.rb +9 -1
  44. data/lib/dry/types/extensions/monads.rb +29 -0
  45. data/lib/dry/types/hash.rb +10 -11
  46. data/lib/dry/types/hash/constructor.rb +5 -5
  47. data/lib/dry/types/json.rb +4 -0
  48. data/lib/dry/types/lax.rb +4 -4
  49. data/lib/dry/types/map.rb +8 -4
  50. data/lib/dry/types/module.rb +3 -3
  51. data/lib/dry/types/nominal.rb +3 -4
  52. data/lib/dry/types/params.rb +9 -0
  53. data/lib/dry/types/predicate_inferrer.rb +197 -0
  54. data/lib/dry/types/predicate_registry.rb +34 -0
  55. data/lib/dry/types/primitive_inferrer.rb +97 -0
  56. data/lib/dry/types/printer.rb +17 -12
  57. data/lib/dry/types/schema.rb +14 -20
  58. data/lib/dry/types/schema/key.rb +19 -1
  59. data/lib/dry/types/spec/types.rb +3 -6
  60. data/lib/dry/types/version.rb +1 -1
  61. metadata +79 -52
@@ -0,0 +1,31 @@
1
+ ---
2
+ title: Constraints
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ You can create constrained types that will use validation rules to check that the input is not violating any of the configured constraints. You can treat it as a lower level guarantee that you're not instantiating objects that are broken.
8
+
9
+ All types support the constraints API, but not all constraints are suitable for a particular primitive, it's up to you to set up constraints that make sense.
10
+
11
+ Under the hood it uses [`dry-logic`](/gems/dry-logic) and all of its predicates are supported.
12
+
13
+ ``` ruby
14
+ string = Types::String.constrained(min_size: 3)
15
+
16
+ string['foo']
17
+ # => "foo"
18
+
19
+ string['fo']
20
+ # => Dry::Types::ConstraintError: "fo" violates constraints
21
+
22
+ email = Types::String.constrained(
23
+ format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
24
+ )
25
+
26
+ email["jane@doe.org"]
27
+ # => "jane@doe.org"
28
+
29
+ email["jane"]
30
+ # => Dry::Types::ConstraintError: "jane" violates constraints
31
+ ```
@@ -0,0 +1,93 @@
1
+ ---
2
+ title: Custom Types
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ There are a bunch of helpers for building your own types based on existing classes and values. These helpers are automatically defined if you're imported types in a module.
8
+
9
+ ### `Types.Instance`
10
+
11
+ `Types.Instance` builds a type that checks if a value has the given class.
12
+
13
+ ```ruby
14
+ range_type = Types.Instance(Range)
15
+ range_type[1..2] # => 1..2
16
+ ```
17
+
18
+ ### `Types.Value`
19
+
20
+ `Types.Value` builds a type that checks a value for equality (using `==`).
21
+
22
+ ```ruby
23
+ valid = Types.Value('valid')
24
+ valid['valid'] # => 'valid'
25
+ valid['invalid']
26
+ # => Dry::Types::ConstraintError: "invalid" violates constraints (eql?("valid", "invalid") failed)
27
+ ```
28
+
29
+ ### `Types.Constant`
30
+
31
+ `Types.Constant` builds a type that checks a value for identity (using `equal?`).
32
+
33
+ ```ruby
34
+ valid = Types.Constant(:valid)
35
+ valid[:valid] # => :valid
36
+ valid[:invalid]
37
+ # => Dry::Types::ConstraintError: :invalid violates constraints (is?(:valid, :invalid) failed)
38
+ ```
39
+
40
+ ### `Types.Constructor`
41
+
42
+ `Types.Constructor` builds a new constructor type for the given class. By default uses the `new` method as a constructor.
43
+
44
+ ```ruby
45
+ user_type = Types.Constructor(User)
46
+
47
+ # It is equivalent to User.new(name: 'John')
48
+ user_type[name: 'John']
49
+
50
+ # Using a block
51
+ user_type = Types.Constructor(User) { |values| User.new(values) }
52
+ ```
53
+
54
+ ### `Types.Nominal`
55
+
56
+ `Types.Nominal` wraps the given class with a simple definition without any behavior attached.
57
+
58
+ ```ruby
59
+ int = Types.Nominal(Integer)
60
+ int[1] # => 1
61
+
62
+ # The type doesn't have any checks
63
+ int['one'] # => 'one'
64
+ ```
65
+
66
+ ### `Types.Hash`
67
+
68
+ `Types.Hash` builds a new hash schema.
69
+
70
+ ```ruby
71
+ # In the full form
72
+ Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer)
73
+
74
+ # Using Types.Hash()
75
+ Types.Hash(:permissive, name: Types::String, age: Types::Coercible::Integer)
76
+ ```
77
+
78
+ ### `Types.Array`
79
+
80
+ `Types.Array` is a shortcut for `Types::Array.of`
81
+
82
+ ```ruby
83
+ ListOfStrings = Types.Array(Types::String)
84
+ ```
85
+
86
+ ### `Types.Interface`
87
+
88
+ `Types.Interface` builds a type that checks a value responds to given methods.
89
+
90
+ ```ruby
91
+ Callable = Types.Interface(:call)
92
+ Contact = Types.Interface(:name, :phone)
93
+ ```
@@ -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,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](/gems/dry-types/1.0/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
+ ```