dry-types 1.1.1 → 1.3.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +381 -208
  3. data/LICENSE +17 -17
  4. data/README.md +14 -13
  5. data/dry-types.gemspec +27 -31
  6. data/lib/dry/types.rb +7 -11
  7. data/lib/dry/types/any.rb +2 -2
  8. data/lib/dry/types/array/member.rb +3 -3
  9. data/lib/dry/types/builder.rb +5 -1
  10. data/lib/dry/types/builder_methods.rb +22 -8
  11. data/lib/dry/types/coercions.rb +6 -0
  12. data/lib/dry/types/coercions/params.rb +1 -1
  13. data/lib/dry/types/compiler.rb +2 -2
  14. data/lib/dry/types/constrained.rb +2 -2
  15. data/lib/dry/types/constructor.rb +7 -34
  16. data/lib/dry/types/constructor/function.rb +17 -31
  17. data/lib/dry/types/core.rb +3 -1
  18. data/lib/dry/types/decorator.rb +3 -9
  19. data/lib/dry/types/default.rb +2 -2
  20. data/lib/dry/types/enum.rb +2 -2
  21. data/lib/dry/types/errors.rb +5 -5
  22. data/lib/dry/types/extensions.rb +4 -0
  23. data/lib/dry/types/extensions/maybe.rb +14 -14
  24. data/lib/dry/types/extensions/monads.rb +29 -0
  25. data/lib/dry/types/hash.rb +3 -2
  26. data/lib/dry/types/lax.rb +2 -5
  27. data/lib/dry/types/meta.rb +3 -3
  28. data/lib/dry/types/module.rb +3 -3
  29. data/lib/dry/types/nominal.rb +1 -1
  30. data/lib/dry/types/params.rb +6 -0
  31. data/lib/dry/types/predicate_inferrer.rb +198 -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/result.rb +2 -2
  35. data/lib/dry/types/schema.rb +23 -2
  36. data/lib/dry/types/schema/key.rb +16 -3
  37. data/lib/dry/types/spec/types.rb +14 -1
  38. data/lib/dry/types/sum.rb +5 -5
  39. data/lib/dry/types/version.rb +1 -1
  40. metadata +26 -45
  41. data/.codeclimate.yml +0 -15
  42. data/.gitignore +0 -11
  43. data/.rspec +0 -2
  44. data/.travis.yml +0 -27
  45. data/.yardopts +0 -9
  46. data/CONTRIBUTING.md +0 -29
  47. data/Gemfile +0 -27
  48. data/Rakefile +0 -22
  49. data/benchmarks/hash_schemas.rb +0 -55
  50. data/benchmarks/lax_schema.rb +0 -15
  51. data/benchmarks/profile_invalid_input.rb +0 -15
  52. data/benchmarks/profile_lax_schema_valid.rb +0 -16
  53. data/benchmarks/profile_valid_input.rb +0 -15
  54. data/benchmarks/schema_valid_vs_invalid.rb +0 -21
  55. data/benchmarks/setup.rb +0 -17
@@ -0,0 +1,198 @@
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
+ alias_method :visit_schema, :visit_hash
75
+
76
+ # @api private
77
+ def visit_array(_)
78
+ ARRAY
79
+ end
80
+
81
+ # @api private
82
+ def visit_lax(node)
83
+ visit(node)
84
+ end
85
+
86
+ # @api private
87
+ def visit_constructor(node)
88
+ other, * = node
89
+ visit(other)
90
+ end
91
+
92
+ # @api private
93
+ def visit_enum(node)
94
+ other, * = node
95
+ visit(other)
96
+ end
97
+
98
+ # @api private
99
+ def visit_sum(node)
100
+ left_node, right_node, = node
101
+ left = visit(left_node)
102
+ right = visit(right_node)
103
+
104
+ if left.eql?(NIL)
105
+ right
106
+ else
107
+ [[left, right]]
108
+ end
109
+ end
110
+
111
+ # @api private
112
+ def visit_constrained(node)
113
+ other, rules = node
114
+ predicates = visit(rules)
115
+
116
+ if predicates.empty?
117
+ visit(other)
118
+ else
119
+ [*visit(other), *merge_predicates(predicates)]
120
+ end
121
+ end
122
+
123
+ # @api private
124
+ def visit_any(_)
125
+ EMPTY_ARRAY
126
+ end
127
+
128
+ # @api private
129
+ def visit_and(node)
130
+ left, right = node
131
+ visit(left) + visit(right)
132
+ end
133
+
134
+ # @api private
135
+ def visit_predicate(node)
136
+ pred, args = node
137
+
138
+ if pred.equal?(:type?)
139
+ EMPTY_ARRAY
140
+ elsif registry.key?(pred)
141
+ *curried, _ = args
142
+ values = curried.map { |_, v| v }
143
+
144
+ if values.empty?
145
+ [pred]
146
+ else
147
+ [pred => values[0]]
148
+ end
149
+ else
150
+ EMPTY_ARRAY
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ # @api private
157
+ def merge_predicates(nodes)
158
+ preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
159
+ if predicate.is_a?(::Hash)
160
+ h.update(predicate)
161
+ else
162
+ ps << predicate
163
+ end
164
+ end
165
+
166
+ merged.empty? ? preds : [*preds, merged]
167
+ end
168
+ end
169
+
170
+ # @return [Compiler]
171
+ # @api private
172
+ attr_reader :compiler
173
+
174
+ # @api private
175
+ def initialize(registry = PredicateRegistry.new)
176
+ @compiler = Compiler.new(registry)
177
+ end
178
+
179
+ # Infer predicate identifier from the provided type
180
+ #
181
+ # @param [Type] type
182
+ # @return [Symbol]
183
+ #
184
+ # @api private
185
+ def [](type)
186
+ self.class.fetch_or_store(type) do
187
+ predicates = compiler.visit(type.to_ast)
188
+
189
+ if predicates.is_a?(::Hash)
190
+ predicates
191
+ else
192
+ REDUCED_TYPES[predicates] || predicates
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/logic/predicates'
4
+
5
+ module Dry
6
+ module Types
7
+ # A registry with predicate objects from `Dry::Logic::Predicates`
8
+ #
9
+ # @api private
10
+ class PredicateRegistry
11
+ # @api private
12
+ attr_reader :predicates
13
+
14
+ # @api private
15
+ attr_reader :has_predicate
16
+
17
+ # @api private
18
+ def initialize(predicates = Logic::Predicates)
19
+ @predicates = predicates
20
+ @has_predicate = ::Kernel.instance_method(:respond_to?).bind(@predicates)
21
+ end
22
+
23
+ # @api private
24
+ def [](name)
25
+ predicates[name]
26
+ end
27
+
28
+ # @api private
29
+ def key?(name)
30
+ has_predicate.(name)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/cache'
4
+
5
+ module Dry
6
+ module Types
7
+ # PrimitiveInferrer returns the list of classes matching a type.
8
+ #
9
+ # @api public
10
+ class PrimitiveInferrer
11
+ extend Core::Cache
12
+
13
+ # Compiler reduces type AST into a list of primitives
14
+ #
15
+ # @api private
16
+ class Compiler
17
+ # @api private
18
+ def visit(node)
19
+ meth, rest = node
20
+ public_send(:"visit_#{meth}", rest)
21
+ end
22
+
23
+ # @api private
24
+ def visit_nominal(node)
25
+ type, _ = node
26
+ type
27
+ end
28
+
29
+ # @api private
30
+ def visit_hash(_)
31
+ ::Hash
32
+ end
33
+ alias_method :visit_schema, :visit_hash
34
+
35
+ # @api private
36
+ def visit_array(_)
37
+ ::Array
38
+ end
39
+
40
+ # @api private
41
+ def visit_lax(node)
42
+ visit(node)
43
+ end
44
+
45
+ # @api private
46
+ def visit_constructor(node)
47
+ other, * = node
48
+ visit(other)
49
+ end
50
+
51
+ # @api private
52
+ def visit_enum(node)
53
+ other, * = node
54
+ visit(other)
55
+ end
56
+
57
+ # @api private
58
+ def visit_sum(node)
59
+ left, right = node
60
+
61
+ [visit(left), visit(right)].flatten(1)
62
+ end
63
+
64
+ # @api private
65
+ def visit_constrained(node)
66
+ other, * = node
67
+ visit(other)
68
+ end
69
+
70
+ # @api private
71
+ def visit_any(_)
72
+ ::Object
73
+ end
74
+ end
75
+
76
+ # @return [Compiler]
77
+ # @api private
78
+ attr_reader :compiler
79
+
80
+ # @api private
81
+ def initialize
82
+ @compiler = Compiler.new
83
+ end
84
+
85
+ # Infer primitives from the provided type
86
+ #
87
+ # @return [Array[Class]]
88
+ #
89
+ # @api private
90
+ def [](type)
91
+ self.class.fetch_or_store(type) do
92
+ Array(compiler.visit(type.to_ast)).freeze
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -8,7 +8,7 @@ module Dry
8
8
  #
9
9
  # @api public
10
10
  class Result
11
- include Dry::Equalizer(:input, inspect: false)
11
+ include ::Dry::Equalizer(:input, immutable: true)
12
12
 
13
13
  # @return [Object]
14
14
  attr_reader :input
@@ -43,7 +43,7 @@ module Dry
43
43
  #
44
44
  # @api public
45
45
  class Failure < Result
46
- include Dry::Equalizer(:input, :error, inspect: false)
46
+ include ::Dry::Equalizer(:input, :error, immutable: true)
47
47
 
48
48
  # @return [#to_s]
49
49
  attr_reader :error
@@ -145,7 +145,7 @@ module Dry
145
145
  #
146
146
  # @api public
147
147
  def to_ast(meta: true)
148
- if RUBY_VERSION >= "2.5"
148
+ if RUBY_VERSION >= '2.5'
149
149
  opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
150
150
  else
151
151
  opts = options.select { |k, _|
@@ -179,7 +179,7 @@ module Dry
179
179
  with(strict: strict)
180
180
  end
181
181
 
182
- # Injects a key transformation function
182
+ # Inject a key transformation function
183
183
  #
184
184
  # @param [#call,nil] proc
185
185
  # @param [#call,nil] block
@@ -284,6 +284,27 @@ module Dry
284
284
  Lax.new(schema(keys.map(&:lax)))
285
285
  end
286
286
 
287
+ # Merge given schema keys into current schema
288
+ #
289
+ # A new instance is returned.
290
+ #
291
+ # @param schema [Schema]
292
+ # @return [Schema]
293
+ #
294
+ # @api public
295
+ def merge(other)
296
+ schema(other.keys)
297
+ end
298
+
299
+ # Empty schema with the same options
300
+ #
301
+ # @return [Schema]
302
+ #
303
+ # @api public
304
+ def clear
305
+ with(keys: EMPTY_ARRAY)
306
+ end
307
+
287
308
  private
288
309
 
289
310
  # @param [Array<Dry::Types::Schema::Keys>] keys
@@ -17,7 +17,7 @@ module Dry
17
17
  class Key
18
18
  extend ::Dry::Core::Deprecations[:'dry-types']
19
19
  include Type
20
- include Dry::Equalizer(:name, :type, :options, inspect: false)
20
+ include Dry::Equalizer(:name, :type, :options, inspect: false, immutable: true)
21
21
  include Decorator
22
22
  include Builder
23
23
  include Printable
@@ -31,6 +31,10 @@ module Dry
31
31
  type.meta.fetch(:required) { !type.meta.fetch(:omittable, false) }
32
32
  end
33
33
 
34
+ unless name.is_a?(::Symbol)
35
+ raise ArgumentError, "Schemas can only contain symbol keys, #{name.inspect} given"
36
+ end
37
+
34
38
  super(type, name, required: required, **options)
35
39
  @name = name
36
40
  end
@@ -99,6 +103,15 @@ module Dry
99
103
  __new__(type.lax).required(false)
100
104
  end
101
105
 
106
+ # Make wrapped type optional
107
+ #
108
+ # @return [Key]
109
+ #
110
+ # @api public
111
+ def optional
112
+ __new__(type.optional)
113
+ end
114
+
102
115
  # Dump to internal AST representation
103
116
  #
104
117
  # @return [Array]
@@ -118,8 +131,8 @@ module Dry
118
131
  # @see Dry::Types::Meta#meta
119
132
  #
120
133
  # @api public
121
- def meta(data = nil)
122
- if data.nil? || !data.key?(:omittable)
134
+ def meta(data = Undefined)
135
+ if Undefined.equal?(data) || !data.key?(:omittable)
123
136
  super
124
137
  else
125
138
  self.class.warn(
@@ -109,7 +109,9 @@ RSpec.shared_examples_for Dry::Types::Nominal do
109
109
  end
110
110
  end
111
111
 
112
- RSpec.shared_examples_for 'a constrained type' do |inputs: Object.new|
112
+ RSpec.shared_examples_for 'a constrained type' do |options = { inputs: Object.new }|
113
+ inputs = options[:inputs]
114
+
113
115
  let(:fallback) { Object.new }
114
116
 
115
117
  describe '#call' do
@@ -137,3 +139,14 @@ RSpec.shared_examples_for 'a nominal type' do |inputs: Object.new|
137
139
  end
138
140
  end
139
141
  end
142
+
143
+ RSpec.shared_examples_for 'a composable constructor' do
144
+ describe '#constructor' do
145
+ it 'has aliases for composition' do
146
+ expect(type.method(:append)).to eql(type.method(:constructor))
147
+ expect(type.method(:prepend)).to eql(type.method(:constructor))
148
+ expect(type.method(:<<)).to eql(type.method(:constructor))
149
+ expect(type.method(:>>)).to eql(type.method(:constructor))
150
+ end
151
+ end
152
+ end