dry-types 1.1.0 → 1.3.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +378 -206
  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 +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/result.rb +2 -2
  36. data/lib/dry/types/schema.rb +23 -2
  37. data/lib/dry/types/schema/key.rb +16 -3
  38. data/lib/dry/types/spec/types.rb +14 -1
  39. data/lib/dry/types/sum.rb +5 -5
  40. data/lib/dry/types/version.rb +1 -1
  41. metadata +26 -45
  42. data/.codeclimate.yml +0 -15
  43. data/.gitignore +0 -11
  44. data/.rspec +0 -2
  45. data/.travis.yml +0 -27
  46. data/.yardopts +0 -9
  47. data/CONTRIBUTING.md +0 -29
  48. data/Gemfile +0 -27
  49. data/Rakefile +0 -22
  50. data/benchmarks/hash_schemas.rb +0 -55
  51. data/benchmarks/lax_schema.rb +0 -15
  52. data/benchmarks/profile_invalid_input.rb +0 -15
  53. data/benchmarks/profile_lax_schema_valid.rb +0 -16
  54. data/benchmarks/profile_valid_input.rb +0 -15
  55. data/benchmarks/schema_valid_vs_invalid.rb +0 -21
  56. data/benchmarks/setup.rb +0 -17
@@ -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
@@ -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
@@ -47,13 +47,17 @@ module Dry
47
47
  yield 'Any'
48
48
  end
49
49
 
50
- def visit_array(_)
51
- yield 'Array'
50
+ def visit_array(type)
51
+ visit_options(EMPTY_HASH, type.meta) do |opts|
52
+ yield "Array#{opts}"
53
+ end
52
54
  end
53
55
 
54
56
  def visit_array_member(array)
55
57
  visit(array.member) do |type|
56
- yield "Array<#{type}>"
58
+ visit_options(EMPTY_HASH, array.meta) do |opts|
59
+ yield "Array<#{type}#{opts}>"
60
+ end
57
61
  end
58
62
  end
59
63
 
@@ -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(