dry-types 0.13.2 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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