dry-types 0.13.2 → 1.5.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +763 -233
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +28 -28
  6. data/lib/dry-types.rb +3 -1
  7. data/lib/dry/types.rb +156 -76
  8. data/lib/dry/types/any.rb +32 -12
  9. data/lib/dry/types/array.rb +19 -6
  10. data/lib/dry/types/array/constructor.rb +32 -0
  11. data/lib/dry/types/array/member.rb +75 -16
  12. data/lib/dry/types/builder.rb +131 -15
  13. data/lib/dry/types/builder_methods.rb +49 -20
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/coercions/json.rb +43 -7
  16. data/lib/dry/types/coercions/params.rb +118 -31
  17. data/lib/dry/types/compat.rb +0 -2
  18. data/lib/dry/types/compiler.rb +56 -41
  19. data/lib/dry/types/constrained.rb +81 -32
  20. data/lib/dry/types/constrained/coercible.rb +36 -6
  21. data/lib/dry/types/constraints.rb +18 -4
  22. data/lib/dry/types/constructor.rb +127 -54
  23. data/lib/dry/types/constructor/function.rb +216 -0
  24. data/lib/dry/types/constructor/wrapper.rb +94 -0
  25. data/lib/dry/types/container.rb +7 -0
  26. data/lib/dry/types/core.rb +54 -21
  27. data/lib/dry/types/decorator.rb +38 -17
  28. data/lib/dry/types/default.rb +61 -16
  29. data/lib/dry/types/enum.rb +43 -20
  30. data/lib/dry/types/errors.rb +75 -9
  31. data/lib/dry/types/extensions.rb +7 -1
  32. data/lib/dry/types/extensions/maybe.rb +74 -16
  33. data/lib/dry/types/extensions/monads.rb +29 -0
  34. data/lib/dry/types/fn_container.rb +6 -1
  35. data/lib/dry/types/hash.rb +86 -67
  36. data/lib/dry/types/hash/constructor.rb +33 -0
  37. data/lib/dry/types/inflector.rb +3 -1
  38. data/lib/dry/types/json.rb +18 -16
  39. data/lib/dry/types/lax.rb +75 -0
  40. data/lib/dry/types/map.rb +76 -33
  41. data/lib/dry/types/meta.rb +51 -0
  42. data/lib/dry/types/module.rb +120 -0
  43. data/lib/dry/types/nominal.rb +210 -0
  44. data/lib/dry/types/options.rb +13 -26
  45. data/lib/dry/types/params.rb +39 -25
  46. data/lib/dry/types/predicate_inferrer.rb +238 -0
  47. data/lib/dry/types/predicate_registry.rb +34 -0
  48. data/lib/dry/types/primitive_inferrer.rb +97 -0
  49. data/lib/dry/types/printable.rb +16 -0
  50. data/lib/dry/types/printer.rb +315 -0
  51. data/lib/dry/types/result.rb +29 -3
  52. data/lib/dry/types/schema.rb +408 -0
  53. data/lib/dry/types/schema/key.rb +156 -0
  54. data/lib/dry/types/spec/types.rb +103 -33
  55. data/lib/dry/types/sum.rb +84 -35
  56. data/lib/dry/types/type.rb +49 -0
  57. data/lib/dry/types/version.rb +3 -1
  58. metadata +68 -79
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.travis.yml +0 -29
  62. data/.yardopts +0 -5
  63. data/CONTRIBUTING.md +0 -29
  64. data/Gemfile +0 -24
  65. data/Rakefile +0 -20
  66. data/benchmarks/hash_schemas.rb +0 -51
  67. data/lib/dry/types/compat/form_types.rb +0 -27
  68. data/lib/dry/types/compat/int.rb +0 -14
  69. data/lib/dry/types/definition.rb +0 -113
  70. data/lib/dry/types/hash/schema.rb +0 -199
  71. data/lib/dry/types/hash/schema_builder.rb +0 -75
  72. data/lib/dry/types/safe.rb +0 -59
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/cache"
4
+ require "dry/core/class_attributes"
5
+ require "dry/types/predicate_registry"
6
+
7
+ module Dry
8
+ module Types
9
+ # PredicateInferrer returns the list of predicates used by a type.
10
+ #
11
+ # @api public
12
+ class PredicateInferrer
13
+ extend Core::Cache
14
+
15
+ TYPE_TO_PREDICATE = {
16
+ ::DateTime => :date_time?,
17
+ ::Date => :date?,
18
+ ::Time => :time?,
19
+ ::FalseClass => :false?,
20
+ ::Integer => :int?,
21
+ ::Float => :float?,
22
+ ::NilClass => :nil?,
23
+ ::String => :str?,
24
+ ::TrueClass => :true?,
25
+ ::BigDecimal => :decimal?,
26
+ ::Array => :array?
27
+ }.freeze
28
+
29
+ REDUCED_TYPES = {
30
+ [[[:true?], [:false?]]] => %i[bool?]
31
+ }.freeze
32
+
33
+ HASH = %i[hash?].freeze
34
+
35
+ ARRAY = %i[array?].freeze
36
+
37
+ NIL = %i[nil?].freeze
38
+
39
+ # Compiler reduces type AST into a list of predicates
40
+ #
41
+ # @api private
42
+ class Compiler
43
+ extend Core::ClassAttributes
44
+
45
+ defines :infer_predicate_by_class_name
46
+ infer_predicate_by_class_name nil
47
+
48
+ # @return [PredicateRegistry]
49
+ # @api private
50
+ attr_reader :registry
51
+
52
+ # @api private
53
+ def initialize(registry)
54
+ @registry = registry
55
+ end
56
+
57
+ # @api private
58
+ def infer_predicate(type)
59
+ pred = TYPE_TO_PREDICATE.fetch(type) do
60
+ if type.name.nil? || self.class.infer_predicate_by_class_name.equal?(false)
61
+ nil
62
+ else
63
+ candidate = :"#{type.name.split("::").last.downcase}?"
64
+
65
+ if registry.key?(candidate)
66
+ if self.class.infer_predicate_by_class_name
67
+ candidate
68
+ else
69
+ raise ::KeyError, <<~MESSAGE
70
+ Automatic predicate inferring from class names is deprecated
71
+ and will be removed in dry-types 2.0.
72
+ Use `Dry::Types::PredicateInferrer::Compiler.infer_predicate_by_class_name true`
73
+ to restore the previous behavior
74
+ or `Dry::Types::PredicateInferrer::Compiler.infer_predicate_by_class_name false`
75
+ to explicitly opt-out (i.e. no exception + no inferring).
76
+ Note: for dry-schema and dry-validation use Dry::Schema::PredicateInferrer::Compiler.
77
+ MESSAGE
78
+ end
79
+ else
80
+ nil
81
+ end
82
+ end
83
+ end
84
+
85
+ if pred.nil?
86
+ EMPTY_ARRAY
87
+ else
88
+ [pred]
89
+ end
90
+ end
91
+
92
+ # @api private
93
+ def visit(node)
94
+ meth, rest = node
95
+ public_send(:"visit_#{meth}", rest)
96
+ end
97
+
98
+ # @api private
99
+ def visit_nominal(node)
100
+ type = node[0]
101
+ predicate = infer_predicate(type)
102
+
103
+ if !predicate.empty? && registry.key?(predicate[0])
104
+ predicate
105
+ else
106
+ [type?: type]
107
+ end
108
+ end
109
+
110
+ # @api private
111
+ def visit_hash(_)
112
+ HASH
113
+ end
114
+ alias_method :visit_schema, :visit_hash
115
+
116
+ # @api private
117
+ def visit_array(_)
118
+ ARRAY
119
+ end
120
+
121
+ # @api private
122
+ def visit_lax(node)
123
+ visit(node)
124
+ end
125
+
126
+ # @api private
127
+ def visit_constructor(node)
128
+ other, * = node
129
+ visit(other)
130
+ end
131
+
132
+ # @api private
133
+ def visit_enum(node)
134
+ other, * = node
135
+ visit(other)
136
+ end
137
+
138
+ # @api private
139
+ def visit_sum(node)
140
+ left_node, right_node, = node
141
+ left = visit(left_node)
142
+ right = visit(right_node)
143
+
144
+ if left.eql?(NIL)
145
+ right
146
+ else
147
+ [[left, right]]
148
+ end
149
+ end
150
+
151
+ # @api private
152
+ def visit_constrained(node)
153
+ other, rules = node
154
+ predicates = visit(rules)
155
+
156
+ if predicates.empty?
157
+ visit(other)
158
+ else
159
+ [*visit(other), *merge_predicates(predicates)]
160
+ end
161
+ end
162
+
163
+ # @api private
164
+ def visit_any(_)
165
+ EMPTY_ARRAY
166
+ end
167
+
168
+ # @api private
169
+ def visit_and(node)
170
+ left, right = node
171
+ visit(left) + visit(right)
172
+ end
173
+
174
+ # @api private
175
+ def visit_predicate(node)
176
+ pred, args = node
177
+
178
+ if pred.equal?(:type?)
179
+ EMPTY_ARRAY
180
+ elsif registry.key?(pred)
181
+ *curried, _ = args
182
+ values = curried.map { |_, v| v }
183
+
184
+ if values.empty?
185
+ [pred]
186
+ else
187
+ [pred => values[0]]
188
+ end
189
+ else
190
+ EMPTY_ARRAY
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ # @api private
197
+ def merge_predicates(nodes)
198
+ preds, merged = nodes.each_with_object([[], {}]) do |predicate, (ps, h)|
199
+ if predicate.is_a?(::Hash)
200
+ h.update(predicate)
201
+ else
202
+ ps << predicate
203
+ end
204
+ end
205
+
206
+ merged.empty? ? preds : [*preds, merged]
207
+ end
208
+ end
209
+
210
+ # @return [Compiler]
211
+ # @api private
212
+ attr_reader :compiler
213
+
214
+ # @api private
215
+ def initialize(registry = PredicateRegistry.new)
216
+ @compiler = Compiler.new(registry)
217
+ end
218
+
219
+ # Infer predicate identifier from the provided type
220
+ #
221
+ # @param [Type] type
222
+ # @return [Symbol]
223
+ #
224
+ # @api private
225
+ def [](type)
226
+ self.class.fetch_or_store(type) do
227
+ predicates = compiler.visit(type.to_ast)
228
+
229
+ if predicates.is_a?(::Hash)
230
+ predicates
231
+ else
232
+ REDUCED_TYPES[predicates] || predicates
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
238
+ 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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # @api private
6
+ module Printable
7
+ # @return [String]
8
+ #
9
+ # @api private
10
+ def to_s
11
+ PRINTER.(self) { super }
12
+ end
13
+ alias_method :inspect, :to_s
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,315 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # @api private
6
+ class Printer
7
+ MAPPING = {
8
+ Nominal => :visit_nominal,
9
+ Constructor => :visit_constructor,
10
+ Constrained => :visit_constrained,
11
+ Constrained::Coercible => :visit_constrained,
12
+ Hash => :visit_hash,
13
+ Schema => :visit_schema,
14
+ Schema::Key => :visit_key,
15
+ Map => :visit_map,
16
+ Array => :visit_array,
17
+ Array::Member => :visit_array_member,
18
+ Lax => :visit_lax,
19
+ Enum => :visit_enum,
20
+ Default => :visit_default,
21
+ Default::Callable => :visit_default,
22
+ Sum => :visit_sum,
23
+ Sum::Constrained => :visit_sum,
24
+ Any.class => :visit_any
25
+ }
26
+
27
+ def call(type)
28
+ output = "".dup
29
+ visit(type) { |str| output << str }
30
+ "#<Dry::Types[#{output}]>"
31
+ end
32
+
33
+ def visit(type, &block)
34
+ print_with = MAPPING.fetch(type.class) do
35
+ if type.class < Constructor
36
+ :visit_constructor
37
+ elsif type.is_a?(Type)
38
+ return yield type.inspect
39
+ else
40
+ raise ArgumentError, "Do not know how to print #{type.class}"
41
+ end
42
+ end
43
+ send(print_with, type, &block)
44
+ end
45
+
46
+ def visit_any(_)
47
+ yield "Any"
48
+ end
49
+
50
+ def visit_array(type)
51
+ visit_options(EMPTY_HASH, type.meta) do |opts|
52
+ yield "Array#{opts}"
53
+ end
54
+ end
55
+
56
+ def visit_array_member(array)
57
+ visit(array.member) do |type|
58
+ visit_options(EMPTY_HASH, array.meta) do |opts|
59
+ yield "Array<#{type}#{opts}>"
60
+ end
61
+ end
62
+ end
63
+
64
+ def visit_constructor(constructor)
65
+ visit(constructor.type) do |type|
66
+ visit_callable(constructor.fn.fn) do |fn|
67
+ options = constructor.options.dup
68
+ options.delete(:fn)
69
+
70
+ visit_options(options) do |opts|
71
+ yield "Constructor<#{type} fn=#{fn}#{opts}>"
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def visit_constrained(constrained)
78
+ visit(constrained.type) do |type|
79
+ options = constrained.options.dup
80
+ rule = options.delete(:rule)
81
+
82
+ visit_options(options) do |_opts|
83
+ yield "Constrained<#{type} rule=[#{rule}]>"
84
+ end
85
+ end
86
+ end
87
+
88
+ def visit_schema(schema)
89
+ options = schema.options.dup
90
+ size = schema.count
91
+ key_fn_str = ""
92
+ type_fn_str = ""
93
+ strict_str = ""
94
+
95
+ strict_str = "strict " if options.delete(:strict)
96
+
97
+ if key_fn = options.delete(:key_transform_fn)
98
+ visit_callable(key_fn) do |fn|
99
+ key_fn_str = "key_fn=#{fn} "
100
+ end
101
+ end
102
+
103
+ if type_fn = options.delete(:type_transform_fn)
104
+ visit_callable(type_fn) do |fn|
105
+ type_fn_str = "type_fn=#{fn} "
106
+ end
107
+ end
108
+
109
+ keys = options.delete(:keys)
110
+
111
+ visit_options(options, schema.meta) do |opts|
112
+ opts = "#{opts[1..-1]} " unless opts.empty?
113
+ schema_parameters = "#{key_fn_str}#{type_fn_str}#{strict_str}#{opts}"
114
+
115
+ header = "Schema<#{schema_parameters}keys={"
116
+
117
+ if size.zero?
118
+ yield "#{header}}>"
119
+ else
120
+ yield header.dup << keys.map { |key|
121
+ visit(key) { |type| type }
122
+ }.join(" ") << "}>"
123
+ end
124
+ end
125
+ end
126
+
127
+ def visit_map(map)
128
+ visit(map.key_type) do |key|
129
+ visit(map.value_type) do |value|
130
+ options = map.options.dup
131
+ options.delete(:key_type)
132
+ options.delete(:value_type)
133
+
134
+ visit_options(options) do |_opts|
135
+ yield "Map<#{key} => #{value}>"
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def visit_key(key)
142
+ visit(key.type) do |type|
143
+ if key.required?
144
+ yield "#{key.name}: #{type}"
145
+ else
146
+ yield "#{key.name}?: #{type}"
147
+ end
148
+ end
149
+ end
150
+
151
+ def visit_sum(sum)
152
+ visit_sum_constructors(sum) do |constructors|
153
+ visit_options(sum.options, sum.meta) do |opts|
154
+ yield "Sum<#{constructors}#{opts}>"
155
+ end
156
+ end
157
+ end
158
+
159
+ def visit_sum_constructors(sum)
160
+ case sum.left
161
+ when Sum
162
+ visit_sum_constructors(sum.left) do |left|
163
+ case sum.right
164
+ when Sum
165
+ visit_sum_constructors(sum.right) do |right|
166
+ yield "#{left} | #{right}"
167
+ end
168
+ else
169
+ visit(sum.right) do |right|
170
+ yield "#{left} | #{right}"
171
+ end
172
+ end
173
+ end
174
+ else
175
+ visit(sum.left) do |left|
176
+ case sum.right
177
+ when Sum
178
+ visit_sum_constructors(sum.right) do |right|
179
+ yield "#{left} | #{right}"
180
+ end
181
+ else
182
+ visit(sum.right) do |right|
183
+ yield "#{left} | #{right}"
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ def visit_enum(enum)
191
+ visit(enum.type) do |type|
192
+ options = enum.options.dup
193
+ mapping = options.delete(:mapping)
194
+
195
+ visit_options(options) do |opts|
196
+ if mapping == enum.inverted_mapping
197
+ values = mapping.values.map(&:inspect).join(", ")
198
+ yield "Enum<#{type} values={#{values}}#{opts}>"
199
+ else
200
+ mapping_str = mapping.map { |key, value|
201
+ "#{key.inspect}=>#{value.inspect}"
202
+ }.join(", ")
203
+ yield "Enum<#{type} mapping={#{mapping_str}}#{opts}>"
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def visit_default(default)
210
+ visit(default.type) do |type|
211
+ visit_options(default.options) do |opts|
212
+ if default.is_a?(Default::Callable)
213
+ visit_callable(default.value) do |fn|
214
+ yield "Default<#{type} value_fn=#{fn}#{opts}>"
215
+ end
216
+ else
217
+ yield "Default<#{type} value=#{default.value.inspect}#{opts}>"
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ def visit_nominal(type)
224
+ visit_options(type.options, type.meta) do |opts|
225
+ yield "Nominal<#{type.primitive}#{opts}>"
226
+ end
227
+ end
228
+
229
+ def visit_lax(lax)
230
+ visit(lax.type) do |type|
231
+ yield "Lax<#{type}>"
232
+ end
233
+ end
234
+
235
+ def visit_hash(hash)
236
+ options = hash.options.dup
237
+ type_fn_str = ""
238
+
239
+ if type_fn = options.delete(:type_transform_fn)
240
+ visit_callable(type_fn) do |fn|
241
+ type_fn_str = "type_fn=#{fn}"
242
+ end
243
+ end
244
+
245
+ visit_options(options, hash.meta) do |opts|
246
+ if opts.empty? && type_fn_str.empty?
247
+ yield "Hash"
248
+ else
249
+ yield "Hash<#{type_fn_str}#{opts}>"
250
+ end
251
+ end
252
+ end
253
+
254
+ def visit_callable(callable)
255
+ fn = callable.is_a?(String) ? FnContainer[callable] : callable
256
+
257
+ case fn
258
+ when ::Method
259
+ yield "#{fn.receiver}.#{fn.name}"
260
+ when ::Proc
261
+ path, line = fn.source_location
262
+
263
+ if line&.zero?
264
+ yield ".#{path}"
265
+ elsif path
266
+ yield "#{path.sub(Dir.pwd + "/", EMPTY_STRING)}:#{line}"
267
+ else
268
+ match = fn.to_s.match(/\A#<Proc:0x\h+\(&:(?<name>\w+)\)(:? \(lambda\))?>\z/)
269
+
270
+ if match
271
+ yield ".#{match[:name]}"
272
+ elsif fn.lambda?
273
+ yield "(lambda)"
274
+ else
275
+ yield "(proc)"
276
+ end
277
+ end
278
+ else
279
+ call = fn.method(:call)
280
+
281
+ if call.owner == fn.class
282
+ yield "#{fn.class}#call"
283
+ else
284
+ yield "#{fn}.call"
285
+ end
286
+ end
287
+ end
288
+
289
+ def visit_options(options, meta = EMPTY_HASH)
290
+ if options.empty? && meta.empty?
291
+ yield ""
292
+ else
293
+ opts = options.empty? ? "" : " options=#{options.inspect}"
294
+
295
+ if meta.empty?
296
+ yield opts
297
+ else
298
+ values = meta.map do |key, value|
299
+ case key
300
+ when Symbol
301
+ "#{key}: #{value.inspect}"
302
+ else
303
+ "#{key.inspect}=>#{value.inspect}"
304
+ end
305
+ end
306
+
307
+ yield "#{opts} meta={#{values.join(", ")}}"
308
+ end
309
+ end
310
+ end
311
+ end
312
+
313
+ PRINTER = Printer.new.freeze
314
+ end
315
+ end