dry-types 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) 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 +68 -0
  7. data/Gemfile +8 -6
  8. data/README.md +2 -2
  9. data/docsite/source/array-with-member.html.md +13 -0
  10. data/docsite/source/built-in-types.html.md +116 -0
  11. data/docsite/source/constraints.html.md +31 -0
  12. data/docsite/source/custom-types.html.md +93 -0
  13. data/docsite/source/default-values.html.md +91 -0
  14. data/docsite/source/enum.html.md +69 -0
  15. data/docsite/source/getting-started.html.md +57 -0
  16. data/docsite/source/hash-schemas.html.md +169 -0
  17. data/docsite/source/index.html.md +155 -0
  18. data/docsite/source/map.html.md +17 -0
  19. data/docsite/source/optional-values.html.md +96 -0
  20. data/docsite/source/sum.html.md +21 -0
  21. data/lib/dry/types.rb +7 -2
  22. data/lib/dry/types/array/member.rb +1 -1
  23. data/lib/dry/types/builder_methods.rb +18 -8
  24. data/lib/dry/types/constructor.rb +2 -29
  25. data/lib/dry/types/decorator.rb +1 -1
  26. data/lib/dry/types/extensions.rb +4 -0
  27. data/lib/dry/types/extensions/monads.rb +29 -0
  28. data/lib/dry/types/hash.rb +2 -1
  29. data/lib/dry/types/lax.rb +1 -1
  30. data/lib/dry/types/params.rb +5 -0
  31. data/lib/dry/types/predicate_inferrer.rb +197 -0
  32. data/lib/dry/types/predicate_registry.rb +34 -0
  33. data/lib/dry/types/primitive_inferrer.rb +97 -0
  34. data/lib/dry/types/printer.rb +7 -3
  35. data/lib/dry/types/version.rb +1 -1
  36. metadata +48 -28
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Map
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ `Map` describes a homogeneous hashmap. This means only types of keys and values are known. You can simply imagine a map input as a list of key-value pairs.
8
+
9
+ ```ruby
10
+ int_float_hash = Types::Hash.map(Types::Integer, Types::Float)
11
+ int_float_hash[100 => 300.0, 42 => 70.0]
12
+ # => {100=>300.0, 42=>70.0}
13
+
14
+ # Only accepts mappings of integers to floats
15
+ int_float_hash[name: 'Jane']
16
+ # => Dry::Types::MapError: input key :name is invalid: type?(Integer, :name)
17
+ ```
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Type Attributes
3
+ layout: gem-single
4
+ name: dry-types
5
+ ---
6
+
7
+ Types themselves have optional attributes you can apply to get further functionality.
8
+
9
+ ### Append `.optional` to a _Type_ to allow `nil`
10
+
11
+ By default, nil values raise an error:
12
+
13
+ ``` ruby
14
+ Types::Strict::String[nil]
15
+ # => raises Dry::Types::ConstraintError
16
+ ```
17
+
18
+ Add `.optional` and `nil` values become valid:
19
+
20
+ ```ruby
21
+ optional_string = Types::Strict::String.optional
22
+
23
+ optional_string[nil]
24
+ # => nil
25
+ optional_string['something']
26
+ # => "something"
27
+ optional_string[123]
28
+ # raises Dry::Types::ConstraintError
29
+ ```
30
+
31
+ `Types::String.optional` is just syntactic sugar for `Types::Strict::Nil | Types::Strict::String`.
32
+
33
+ ### Handle optional values using Monads
34
+
35
+ The [dry-monads gem](/gems/dry-monads/) provides another 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`.
36
+
37
+ > NOTE: Requires the [dry-monads gem](/gems/dry-monads/) to be loaded.
38
+
39
+ 1. Load the `:maybe` extension in your application.
40
+
41
+ ```ruby
42
+ require 'dry-types'
43
+
44
+ Dry::Types.load_extensions(:maybe)
45
+ module Types
46
+ include Dry.Types()
47
+ end
48
+ ```
49
+
50
+ 2. Append `.maybe` to a _Type_ to return a _Monad_ object
51
+
52
+ ```ruby
53
+ x = Types::Maybe::Strict::Integer[nil]
54
+ Maybe(x) { puts(x) }
55
+
56
+ x = Types::Maybe::Coercible::String[nil]
57
+ Maybe(x) { puts(x) }
58
+
59
+ x = Types::Maybe::Strict::Integer[123]
60
+ Maybe(x) { puts(x) }
61
+
62
+ x = Types::Maybe::Strict::String[123]
63
+ Maybe(x) { puts(x) }
64
+ ```
65
+
66
+ ```ruby
67
+ Types::Maybe::Strict::Integer[nil] # None
68
+ Types::Maybe::Strict::Integer[123] # Some(123)
69
+
70
+ Types::Maybe::Coercible::Float[nil] # None
71
+ Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
72
+
73
+ # 'Maybe' types can also accessed by calling '.maybe' on a regular type:
74
+ Types::Strict::Integer.maybe # equivalent to Types::Maybe::Strict::Integer
75
+ ```
76
+
77
+ You can define your own optional types:
78
+
79
+ ``` ruby
80
+ maybe_string = Types::Strict::String.maybe
81
+
82
+ maybe_string[nil]
83
+ # => None
84
+
85
+ maybe_string[nil].fmap(&:upcase)
86
+ # => None
87
+
88
+ maybe_string['something']
89
+ # => Some('something')
90
+
91
+ maybe_string['something'].fmap(&:upcase)
92
+ # => Some('SOMETHING')
93
+
94
+ maybe_string['something'].fmap(&:upcase).value_or('NOTHING')
95
+ # => "SOMETHING"
96
+ ```
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Sum
3
+ layout: gem-single
4
+ name: dry-types
5
+ order: 7
6
+ ---
7
+
8
+ You can specify sum types using `|` operator, it is an explicit way of defining what the valid types of a value are.
9
+
10
+ For example `dry-types` defines the `Bool` type which is a sum consisting of the `True` and `False` types, expressed as `Types::True | Types::False`.
11
+
12
+ Another common case is defining that something can be either `nil` or something else:
13
+
14
+ ``` ruby
15
+ nil_or_string = Types::Nil | Types::String
16
+
17
+ nil_or_string[nil] # => nil
18
+ nil_or_string["hello"] # => "hello"
19
+
20
+ nil_or_string[123] # raises Dry::Types::ConstraintError
21
+ ```
data/lib/dry/types.rb CHANGED
@@ -89,7 +89,7 @@ module Dry
89
89
  def self.[](name)
90
90
  type_map.fetch_or_store(name) do
91
91
  case name
92
- when String
92
+ when ::String
93
93
  result = name.match(TYPE_SPEC_REGEX)
94
94
 
95
95
  if result
@@ -98,7 +98,12 @@ module Dry
98
98
  else
99
99
  container[name]
100
100
  end
101
- when Class
101
+ when ::Class
102
+ warn(<<~DEPRECATION)
103
+ Using Dry::Types.[] with a class is deprecated, please use string identifiers: Dry::Types[Integer] -> Dry::Types['integer'].
104
+ If you're using dry-struct this means changing `attribute :counter, Integer` to `attribute :counter, Dry::Types['integer']` or to `attribute :counter, 'integer'`.
105
+ DEPRECATION
106
+
102
107
  type_name = identifier(name)
103
108
 
104
109
  if container.key?(type_name)
@@ -100,7 +100,7 @@ module Dry
100
100
  #
101
101
  # @api public
102
102
  def lax
103
- Lax.new(Member.new(primitive, { **options, member: member.lax }))
103
+ Lax.new(Member.new(primitive, { **options, member: member.lax, meta: meta }))
104
104
  end
105
105
 
106
106
  # @see Nominal#to_ast
@@ -25,7 +25,7 @@ module Dry
25
25
  #
26
26
  # @return [Dry::Types::Array]
27
27
  def Array(type)
28
- self::Array.of(type)
28
+ Strict(::Array).of(type)
29
29
  end
30
30
 
31
31
  # Build a hash schema
@@ -34,7 +34,7 @@ module Dry
34
34
  #
35
35
  # @return [Dry::Types::Array]
36
36
  def Hash(type_map)
37
- self::Hash.schema(type_map)
37
+ Strict(::Hash).schema(type_map)
38
38
  end
39
39
 
40
40
  # Build a type which values are instances of a given class
@@ -49,7 +49,7 @@ module Dry
49
49
  #
50
50
  # @return [Dry::Types::Type]
51
51
  def Instance(klass)
52
- Nominal.new(klass).constrained(type: klass)
52
+ Nominal(klass).constrained(type: klass)
53
53
  end
54
54
  alias_method :Strict, :Instance
55
55
 
@@ -60,7 +60,7 @@ module Dry
60
60
  #
61
61
  # @return [Dry::Types::Type]
62
62
  def Value(value)
63
- Nominal.new(value.class).constrained(eql: value)
63
+ Nominal(value.class).constrained(eql: value)
64
64
  end
65
65
 
66
66
  # Build a type with a single value
@@ -70,7 +70,7 @@ module Dry
70
70
  #
71
71
  # @return [Dry::Types::Type]
72
72
  def Constant(object)
73
- Nominal.new(object.class).constrained(is: object)
73
+ Nominal(object.class).constrained(is: object)
74
74
  end
75
75
 
76
76
  # Build a constructor type
@@ -82,7 +82,11 @@ module Dry
82
82
  #
83
83
  # @return [Dry::Types::Type]
84
84
  def Constructor(klass, cons = nil, &block)
85
- Nominal.new(klass).constructor(cons || block || klass.method(:new))
85
+ if klass.is_a?(Type)
86
+ klass.constructor(cons || block || klass.method(:new))
87
+ else
88
+ Nominal(klass).constructor(cons || block || klass.method(:new))
89
+ end
86
90
  end
87
91
 
88
92
  # Build a nominal type
@@ -91,7 +95,13 @@ module Dry
91
95
  #
92
96
  # @return [Dry::Types::Type]
93
97
  def Nominal(klass)
94
- Nominal.new(klass)
98
+ if klass <= ::Array
99
+ Array.new(klass)
100
+ elsif klass <= ::Hash
101
+ Hash.new(klass)
102
+ else
103
+ Nominal.new(klass)
104
+ end
95
105
  end
96
106
 
97
107
  # Build a map type
@@ -105,7 +115,7 @@ module Dry
105
115
  #
106
116
  # @return [Dry::Types::Map]
107
117
  def Map(key_type, value_type)
108
- Types['nominal.hash'].map(key_type, value_type)
118
+ Nominal(::Hash).map(key_type, value_type)
109
119
  end
110
120
 
111
121
  # Builds a constrained nominal type accepting any value that
@@ -12,15 +12,13 @@ module Dry
12
12
  class Constructor < Nominal
13
13
  include Dry::Equalizer(:type, :options, inspect: false)
14
14
 
15
- private :meta
16
-
17
15
  # @return [#call]
18
16
  attr_reader :fn
19
17
 
20
18
  # @return [Type]
21
19
  attr_reader :type
22
20
 
23
- undef :constrained?
21
+ undef :constrained?, :meta, :optional?, :primitive, :default?, :name
24
22
 
25
23
  # @param [Builder, Object] input
26
24
  # @param [Hash] options
@@ -46,31 +44,6 @@ module Dry
46
44
  super(type, **options, fn: fn)
47
45
  end
48
46
 
49
- # Return the inner type's primitive
50
- #
51
- # @return [Class]
52
- #
53
- # @api public
54
- def primitive
55
- type.primitive
56
- end
57
-
58
- # Return the inner type's name
59
- #
60
- # @return [String]
61
- #
62
- # @api public
63
- def name
64
- type.name
65
- end
66
-
67
- # @return [Boolean]
68
- #
69
- # @api public
70
- def default?
71
- type.default?
72
- end
73
-
74
47
  # @return [Object]
75
48
  #
76
49
  # @api private
@@ -182,7 +155,7 @@ module Dry
182
155
  # @api private
183
156
  def method_missing(method, *args, &block)
184
157
  if type.respond_to?(method)
185
- response = type.__send__(method, *args, &block)
158
+ response = type.public_send(method, *args, &block)
186
159
 
187
160
  if response.is_a?(Type) && type.class == response.class
188
161
  response.constructor_type.new(response, options)
@@ -90,7 +90,7 @@ module Dry
90
90
  # @api private
91
91
  def method_missing(meth, *args, &block)
92
92
  if type.respond_to?(meth)
93
- response = type.__send__(meth, *args, &block)
93
+ response = type.public_send(meth, *args, &block)
94
94
 
95
95
  if decorate?(response)
96
96
  __new__(response)
@@ -3,3 +3,7 @@
3
3
  Dry::Types.register_extension(:maybe) do
4
4
  require 'dry/types/extensions/maybe'
5
5
  end
6
+
7
+ Dry::Types.register_extension(:monads) do
8
+ require 'dry/types/extensions/monads'
9
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/monads/result'
4
+
5
+ module Dry
6
+ module Types
7
+ # Monad extension for Result
8
+ #
9
+ # @api public
10
+ class Result
11
+ include Dry::Monads::Result::Mixin
12
+
13
+ # Turn result into a monad
14
+ #
15
+ # This makes result objects work with dry-monads (or anything with a compatible interface)
16
+ #
17
+ # @return [Dry::Monads::Success,Dry::Monads::Failure]
18
+ #
19
+ # @api public
20
+ def to_monad
21
+ if success?
22
+ Success(input)
23
+ else
24
+ Failure([error, input])
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -121,7 +121,8 @@ module Dry
121
121
  # @api private
122
122
  def resolve_type(type)
123
123
  case type
124
- when String, Class then Types[type]
124
+ when Type then type
125
+ when ::Class, ::String then Types[type]
125
126
  else type
126
127
  end
127
128
  end
data/lib/dry/types/lax.rb CHANGED
@@ -15,7 +15,7 @@ module Dry
15
15
  include Printable
16
16
  include Dry::Equalizer(:type, inspect: false)
17
17
 
18
- private :options, :constructor
18
+ undef :options, :constructor
19
19
 
20
20
  # @param [Object] input
21
21
  #
@@ -55,5 +55,10 @@ module Dry
55
55
  register('params.symbol') do
56
56
  self['nominal.symbol'].constructor(Coercions::Params.method(:to_symbol))
57
57
  end
58
+
59
+ COERCIBLE.each_key do |name|
60
+ next if name.equal?(:string)
61
+ register("optional.params.#{name}", self['params.nil'] | self["params.#{name}"])
62
+ end
58
63
  end
59
64
  end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/cache'
4
+ require 'dry/types/predicate_registry'
5
+
6
+ module Dry
7
+ module Types
8
+ # PredicateInferrer returns the list of predicates used by a type.
9
+ #
10
+ # @api public
11
+ class PredicateInferrer
12
+ extend Core::Cache
13
+
14
+ TYPE_TO_PREDICATE = {
15
+ DateTime => :date_time?,
16
+ FalseClass => :false?,
17
+ Integer => :int?,
18
+ NilClass => :nil?,
19
+ String => :str?,
20
+ TrueClass => :true?,
21
+ BigDecimal => :decimal?
22
+ }.freeze
23
+
24
+ REDUCED_TYPES = {
25
+ [[[:true?], [:false?]]] => %i[bool?]
26
+ }.freeze
27
+
28
+ HASH = %i[hash?].freeze
29
+
30
+ ARRAY = %i[array?].freeze
31
+
32
+ NIL = %i[nil?].freeze
33
+
34
+ # Compiler reduces type AST into a list of predicates
35
+ #
36
+ # @api private
37
+ class Compiler
38
+ # @return [PredicateRegistry]
39
+ # @api private
40
+ attr_reader :registry
41
+
42
+ # @api private
43
+ def initialize(registry)
44
+ @registry = registry
45
+ end
46
+
47
+ # @api private
48
+ def infer_predicate(type)
49
+ [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
50
+ end
51
+
52
+ # @api private
53
+ def visit(node)
54
+ meth, rest = node
55
+ public_send(:"visit_#{meth}", rest)
56
+ end
57
+
58
+ # @api private
59
+ def visit_nominal(node)
60
+ type = node[0]
61
+ predicate = infer_predicate(type)
62
+
63
+ if registry.key?(predicate[0])
64
+ predicate
65
+ else
66
+ [type?: type]
67
+ end
68
+ end
69
+
70
+ # @api private
71
+ def visit_hash(_)
72
+ HASH
73
+ end
74
+
75
+ # @api private
76
+ def visit_array(_)
77
+ ARRAY
78
+ end
79
+
80
+ # @api private
81
+ def visit_lax(node)
82
+ visit(node)
83
+ end
84
+
85
+ # @api private
86
+ def visit_constructor(node)
87
+ other, * = node
88
+ visit(other)
89
+ end
90
+
91
+ # @api private
92
+ def visit_enum(node)
93
+ other, * = node
94
+ visit(other)
95
+ end
96
+
97
+ # @api private
98
+ def visit_sum(node)
99
+ left_node, right_node, = node
100
+ left = visit(left_node)
101
+ right = visit(right_node)
102
+
103
+ if left.eql?(NIL)
104
+ right
105
+ else
106
+ [[left, right]]
107
+ end
108
+ end
109
+
110
+ # @api private
111
+ def visit_constrained(node)
112
+ other, rules = node
113
+ predicates = visit(rules)
114
+
115
+ if predicates.empty?
116
+ visit(other)
117
+ else
118
+ [*visit(other), *merge_predicates(predicates)]
119
+ end
120
+ end
121
+
122
+ # @api private
123
+ def visit_any(_)
124
+ EMPTY_ARRAY
125
+ end
126
+
127
+ # @api private
128
+ def visit_and(node)
129
+ left, right = node
130
+ visit(left) + visit(right)
131
+ end
132
+
133
+ # @api private
134
+ def visit_predicate(node)
135
+ pred, args = node
136
+
137
+ if pred.equal?(:type?)
138
+ EMPTY_ARRAY
139
+ elsif registry.key?(pred)
140
+ *curried, _ = args
141
+ values = curried.map { |_, v| v }
142
+
143
+ if values.empty?
144
+ [pred]
145
+ else
146
+ [pred => values[0]]
147
+ end
148
+ else
149
+ EMPTY_ARRAY
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ # @api private
156
+ def merge_predicates(nodes)
157
+ preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
158
+ if predicate.is_a?(::Hash)
159
+ h.update(predicate)
160
+ else
161
+ ps << predicate
162
+ end
163
+ end
164
+
165
+ merged.empty? ? preds : [*preds, merged]
166
+ end
167
+ end
168
+
169
+ # @return [Compiler]
170
+ # @api private
171
+ attr_reader :compiler
172
+
173
+ # @api private
174
+ def initialize(registry = PredicateRegistry.new)
175
+ @compiler = Compiler.new(registry)
176
+ end
177
+
178
+ # Infer predicate identifier from the provided type
179
+ #
180
+ # @param [Type] type
181
+ # @return [Symbol]
182
+ #
183
+ # @api private
184
+ def [](type)
185
+ self.class.fetch_or_store(type) do
186
+ predicates = compiler.visit(type.to_ast)
187
+
188
+ if predicates.is_a?(::Hash)
189
+ predicates
190
+ else
191
+ REDUCED_TYPES[predicates] || predicates
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end