dry-types 1.1.0 → 1.2.0

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