dry-types 1.4.0 → 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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -0
  3. data/LICENSE +1 -1
  4. data/README.md +1 -1
  5. data/dry-types.gemspec +2 -3
  6. data/lib/dry-types.rb +1 -1
  7. data/lib/dry/types.rb +55 -31
  8. data/lib/dry/types/any.rb +2 -2
  9. data/lib/dry/types/array.rb +2 -2
  10. data/lib/dry/types/array/constructor.rb +1 -1
  11. data/lib/dry/types/array/member.rb +1 -1
  12. data/lib/dry/types/builder.rb +66 -18
  13. data/lib/dry/types/builder_methods.rb +1 -2
  14. data/lib/dry/types/coercions/json.rb +5 -5
  15. data/lib/dry/types/coercions/params.rb +3 -3
  16. data/lib/dry/types/compiler.rb +10 -10
  17. data/lib/dry/types/constrained.rb +6 -9
  18. data/lib/dry/types/constraints.rb +3 -3
  19. data/lib/dry/types/constructor.rb +40 -6
  20. data/lib/dry/types/constructor/function.rb +32 -2
  21. data/lib/dry/types/constructor/wrapper.rb +94 -0
  22. data/lib/dry/types/container.rb +1 -1
  23. data/lib/dry/types/core.rb +12 -12
  24. data/lib/dry/types/decorator.rb +2 -2
  25. data/lib/dry/types/default.rb +14 -1
  26. data/lib/dry/types/enum.rb +4 -3
  27. data/lib/dry/types/errors.rb +1 -1
  28. data/lib/dry/types/extensions.rb +2 -2
  29. data/lib/dry/types/extensions/maybe.rb +5 -4
  30. data/lib/dry/types/extensions/monads.rb +1 -1
  31. data/lib/dry/types/fn_container.rb +1 -1
  32. data/lib/dry/types/hash.rb +9 -15
  33. data/lib/dry/types/hash/constructor.rb +1 -1
  34. data/lib/dry/types/inflector.rb +1 -1
  35. data/lib/dry/types/json.rb +15 -15
  36. data/lib/dry/types/lax.rb +2 -2
  37. data/lib/dry/types/map.rb +2 -2
  38. data/lib/dry/types/meta.rb +1 -1
  39. data/lib/dry/types/module.rb +6 -6
  40. data/lib/dry/types/nominal.rb +11 -11
  41. data/lib/dry/types/params.rb +30 -28
  42. data/lib/dry/types/predicate_inferrer.rb +51 -11
  43. data/lib/dry/types/predicate_registry.rb +1 -1
  44. data/lib/dry/types/primitive_inferrer.rb +1 -1
  45. data/lib/dry/types/printer.rb +25 -25
  46. data/lib/dry/types/result.rb +1 -1
  47. data/lib/dry/types/schema.rb +5 -13
  48. data/lib/dry/types/schema/key.rb +4 -4
  49. data/lib/dry/types/spec/types.rb +57 -45
  50. data/lib/dry/types/sum.rb +4 -3
  51. data/lib/dry/types/type.rb +1 -1
  52. data/lib/dry/types/version.rb +1 -1
  53. metadata +9 -22
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
4
- require 'dry/types/predicate_registry'
3
+ require "dry/core/cache"
4
+ require "dry/core/class_attributes"
5
+ require "dry/types/predicate_registry"
5
6
 
6
7
  module Dry
7
8
  module Types
@@ -12,13 +13,17 @@ module Dry
12
13
  extend Core::Cache
13
14
 
14
15
  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?
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?
22
27
  }.freeze
23
28
 
24
29
  REDUCED_TYPES = {
@@ -35,6 +40,11 @@ module Dry
35
40
  #
36
41
  # @api private
37
42
  class Compiler
43
+ extend Core::ClassAttributes
44
+
45
+ defines :infer_predicate_by_class_name
46
+ infer_predicate_by_class_name nil
47
+
38
48
  # @return [PredicateRegistry]
39
49
  # @api private
40
50
  attr_reader :registry
@@ -46,7 +56,37 @@ module Dry
46
56
 
47
57
  # @api private
48
58
  def infer_predicate(type)
49
- [TYPE_TO_PREDICATE.fetch(type) { :"#{type.name.split('::').last.downcase}?" }]
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
50
90
  end
51
91
 
52
92
  # @api private
@@ -60,7 +100,7 @@ module Dry
60
100
  type = node[0]
61
101
  predicate = infer_predicate(type)
62
102
 
63
- if registry.key?(predicate[0])
103
+ if !predicate.empty? && registry.key?(predicate[0])
64
104
  predicate
65
105
  else
66
106
  [type?: type]
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/logic/predicates'
3
+ require "dry/logic/predicates"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/core/cache'
3
+ require "dry/core/cache"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -7,8 +7,6 @@ module Dry
7
7
  MAPPING = {
8
8
  Nominal => :visit_nominal,
9
9
  Constructor => :visit_constructor,
10
- Hash::Constructor => :visit_constructor,
11
- Array::Constructor => :visit_constructor,
12
10
  Constrained => :visit_constrained,
13
11
  Constrained::Coercible => :visit_constrained,
14
12
  Hash => :visit_hash,
@@ -27,14 +25,16 @@ module Dry
27
25
  }
28
26
 
29
27
  def call(type)
30
- output = ''.dup
28
+ output = "".dup
31
29
  visit(type) { |str| output << str }
32
30
  "#<Dry::Types[#{output}]>"
33
31
  end
34
32
 
35
33
  def visit(type, &block)
36
34
  print_with = MAPPING.fetch(type.class) do
37
- if type.is_a?(Type)
35
+ if type.class < Constructor
36
+ :visit_constructor
37
+ elsif type.is_a?(Type)
38
38
  return yield type.inspect
39
39
  else
40
40
  raise ArgumentError, "Do not know how to print #{type.class}"
@@ -44,7 +44,7 @@ module Dry
44
44
  end
45
45
 
46
46
  def visit_any(_)
47
- yield 'Any'
47
+ yield "Any"
48
48
  end
49
49
 
50
50
  def visit_array(type)
@@ -88,11 +88,11 @@ module Dry
88
88
  def visit_schema(schema)
89
89
  options = schema.options.dup
90
90
  size = schema.count
91
- key_fn_str = ''
92
- type_fn_str = ''
93
- strict_str = ''
91
+ key_fn_str = ""
92
+ type_fn_str = ""
93
+ strict_str = ""
94
94
 
95
- strict_str = 'strict ' if options.delete(:strict)
95
+ strict_str = "strict " if options.delete(:strict)
96
96
 
97
97
  if key_fn = options.delete(:key_transform_fn)
98
98
  visit_callable(key_fn) do |fn|
@@ -119,7 +119,7 @@ module Dry
119
119
  else
120
120
  yield header.dup << keys.map { |key|
121
121
  visit(key) { |type| type }
122
- }.join(' ') << '}>'
122
+ }.join(" ") << "}>"
123
123
  end
124
124
  end
125
125
  end
@@ -194,12 +194,12 @@ module Dry
194
194
 
195
195
  visit_options(options) do |opts|
196
196
  if mapping == enum.inverted_mapping
197
- values = mapping.values.map(&:inspect).join(', ')
197
+ values = mapping.values.map(&:inspect).join(", ")
198
198
  yield "Enum<#{type} values={#{values}}#{opts}>"
199
199
  else
200
200
  mapping_str = mapping.map { |key, value|
201
201
  "#{key.inspect}=>#{value.inspect}"
202
- }.join(', ')
202
+ }.join(", ")
203
203
  yield "Enum<#{type} mapping={#{mapping_str}}#{opts}>"
204
204
  end
205
205
  end
@@ -234,7 +234,7 @@ module Dry
234
234
 
235
235
  def visit_hash(hash)
236
236
  options = hash.options.dup
237
- type_fn_str = ''
237
+ type_fn_str = ""
238
238
 
239
239
  if type_fn = options.delete(:type_transform_fn)
240
240
  visit_callable(type_fn) do |fn|
@@ -244,7 +244,7 @@ module Dry
244
244
 
245
245
  visit_options(options, hash.meta) do |opts|
246
246
  if opts.empty? && type_fn_str.empty?
247
- yield 'Hash'
247
+ yield "Hash"
248
248
  else
249
249
  yield "Hash<#{type_fn_str}#{opts}>"
250
250
  end
@@ -255,24 +255,24 @@ module Dry
255
255
  fn = callable.is_a?(String) ? FnContainer[callable] : callable
256
256
 
257
257
  case fn
258
- when Method
258
+ when ::Method
259
259
  yield "#{fn.receiver}.#{fn.name}"
260
- when Proc
260
+ when ::Proc
261
261
  path, line = fn.source_location
262
262
 
263
263
  if line&.zero?
264
264
  yield ".#{path}"
265
265
  elsif path
266
- yield "#{path.sub(Dir.pwd + '/', EMPTY_STRING)}:#{line}"
267
- elsif fn.lambda?
268
- yield '(lambda)'
266
+ yield "#{path.sub(Dir.pwd + "/", EMPTY_STRING)}:#{line}"
269
267
  else
270
- match = fn.to_s.match(/\A#<Proc:0x\h+\(&:(\w+)\)>\z/)
268
+ match = fn.to_s.match(/\A#<Proc:0x\h+\(&:(?<name>\w+)\)(:? \(lambda\))?>\z/)
271
269
 
272
270
  if match
273
- yield ".#{match[1]}"
271
+ yield ".#{match[:name]}"
272
+ elsif fn.lambda?
273
+ yield "(lambda)"
274
274
  else
275
- yield '(proc)'
275
+ yield "(proc)"
276
276
  end
277
277
  end
278
278
  else
@@ -288,9 +288,9 @@ module Dry
288
288
 
289
289
  def visit_options(options, meta = EMPTY_HASH)
290
290
  if options.empty? && meta.empty?
291
- yield ''
291
+ yield ""
292
292
  else
293
- opts = options.empty? ? '' : " options=#{options.inspect}"
293
+ opts = options.empty? ? "" : " options=#{options.inspect}"
294
294
 
295
295
  if meta.empty?
296
296
  yield opts
@@ -304,7 +304,7 @@ module Dry
304
304
  end
305
305
  end
306
306
 
307
- yield "#{opts} meta={#{values.join(', ')}}"
307
+ yield "#{opts} meta={#{values.join(", ")}}"
308
308
  end
309
309
  end
310
310
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
3
+ require "dry/core/equalizer"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/types/fn_container'
3
+ require "dry/types/fn_container"
4
4
 
5
5
  module Dry
6
6
  module Types
@@ -83,7 +83,7 @@ module Dry
83
83
  call_unsafe(hash, options)
84
84
  end
85
85
 
86
- # @param [Hash] hash
86
+ # @param input [Hash] hash
87
87
  #
88
88
  # @yieldparam [Failure] failure
89
89
  # @yieldreturn [Result]
@@ -145,18 +145,10 @@ module Dry
145
145
  #
146
146
  # @api public
147
147
  def to_ast(meta: true)
148
- if RUBY_VERSION >= '2.5'
149
- opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
150
- else
151
- opts = options.select { |k, _|
152
- k == :key_transform_fn || k == :type_transform_fn || k == :strict
153
- }
154
- end
155
-
156
148
  [
157
149
  :schema,
158
150
  [keys.map { |key| key.to_ast(meta: meta) },
159
- opts,
151
+ options.slice(:key_transform_fn, :type_transform_fn, :strict),
160
152
  meta ? self.meta : EMPTY_HASH]
161
153
  ]
162
154
  end
@@ -190,7 +182,7 @@ module Dry
190
182
  def with_key_transform(proc = nil, &block)
191
183
  fn = proc || block
192
184
 
193
- raise ArgumentError, 'a block or callable argument is required' if fn.nil?
185
+ raise ArgumentError, "a block or callable argument is required" if fn.nil?
194
186
 
195
187
  handle = Dry::Types::FnContainer.register(fn)
196
188
  with(key_transform_fn: handle)
@@ -288,7 +280,7 @@ module Dry
288
280
  #
289
281
  # A new instance is returned.
290
282
  #
291
- # @param schema [Schema]
283
+ # @param other [Schema] schema
292
284
  # @return [Schema]
293
285
  #
294
286
  # @api public
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/equalizer'
4
- require 'dry/core/deprecations'
3
+ require "dry/core/equalizer"
4
+ require "dry/core/deprecations"
5
5
 
6
6
  module Dry
7
7
  module Types
@@ -136,8 +136,8 @@ module Dry
136
136
  super
137
137
  else
138
138
  self.class.warn(
139
- 'Using meta for making schema keys is deprecated, ' \
140
- 'please use .omittable or .required(false) instead' \
139
+ "Using meta for making schema keys is deprecated, " \
140
+ "please use .omittable or .required(false) instead" \
141
141
  "\n" + Core::Deprecations::STACK.()
142
142
  )
143
143
  super.required(!data[:omittable])
@@ -1,79 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.shared_examples_for 'Dry::Types::Nominal without primitive' do
3
+ RSpec.shared_examples_for "Dry::Types::Nominal without primitive" do
4
4
  def be_boolean
5
5
  satisfy { |x| x == true || x == false }
6
6
  end
7
7
 
8
- describe '#constrained?' do
9
- it 'returns a boolean value' do
8
+ describe "#constrained?" do
9
+ it "returns a boolean value" do
10
10
  expect(type.constrained?).to be_boolean
11
11
  end
12
12
  end
13
13
 
14
- describe '#default?' do
15
- it 'returns a boolean value' do
14
+ describe "#default?" do
15
+ it "returns a boolean value" do
16
16
  expect(type.default?).to be_boolean
17
17
  end
18
18
  end
19
19
 
20
- describe '#valid?' do
21
- it 'returns a boolean value' do
20
+ describe "#valid?" do
21
+ it "returns a boolean value" do
22
22
  expect(type.valid?(1)).to be_boolean
23
23
  end
24
24
  end
25
25
 
26
- describe '#eql?' do
27
- it 'has #eql? defined' do
26
+ describe "#eql?" do
27
+ it "has #eql? defined" do
28
28
  expect(type).to eql(type)
29
29
  end
30
30
  end
31
31
 
32
- describe '#==' do
33
- it 'has #== defined' do
32
+ describe "#==" do
33
+ it "has #== defined" do
34
34
  expect(type).to eq(type)
35
35
  end
36
36
  end
37
37
 
38
- describe '#optional?' do
39
- it 'returns a boolean value' do
38
+ describe "#optional?" do
39
+ it "returns a boolean value" do
40
40
  expect(type.optional?).to be_boolean
41
41
  end
42
42
  end
43
43
 
44
- describe '#to_s' do
45
- it 'returns a custom string representation' do
46
- expect(type.to_s).to start_with('#<Dry::Types') if type.class.name.start_with?('Dry::Types')
44
+ describe "#to_s" do
45
+ it "returns a custom string representation" do
46
+ expect(type.to_s).to start_with("#<Dry::Types") if type.class.name.start_with?("Dry::Types")
47
47
  end
48
48
  end
49
49
 
50
- describe '#to_proc' do
50
+ describe "#to_proc" do
51
51
  subject(:callable) { type.to_proc }
52
52
 
53
- it 'converts a type to a proc' do
53
+ it "converts a type to a proc" do
54
54
  expect(callable).to be_a(Proc)
55
55
  end
56
56
  end
57
57
  end
58
58
 
59
- RSpec.shared_examples_for 'Dry::Types::Nominal#meta' do
60
- describe '#meta' do
61
- it 'allows setting meta information' do
62
- with_meta = type.meta(foo: :bar).meta(baz: '1')
59
+ RSpec.shared_examples_for "Dry::Types::Nominal#meta" do
60
+ describe "#meta" do
61
+ it "allows setting meta information" do
62
+ with_meta = type.meta(foo: :bar).meta(baz: "1")
63
63
 
64
64
  expect(with_meta).to be_instance_of(type.class)
65
- expect(with_meta.meta).to eql(foo: :bar, baz: '1')
65
+ expect(with_meta.meta).to eql(foo: :bar, baz: "1")
66
66
  end
67
67
 
68
- it 'equalizes on empty meta' do
68
+ it "equalizes on empty meta" do
69
69
  expect(type).to eql(type.meta({}))
70
70
  end
71
71
 
72
- it 'equalizes on filled meta' do
73
- expect(type).to_not eql(type.meta(i_am: 'different'))
72
+ it "equalizes on filled meta" do
73
+ expect(type).to_not eql(type.meta(i_am: "different"))
74
74
  end
75
75
 
76
- it 'is locally immutable' do
76
+ it "is locally immutable" do
77
77
  expect(type.meta).to be_a ::Hash
78
78
  expect(type.meta).to be_frozen
79
79
  expect(type.meta).not_to have_key :immutable_test
@@ -84,24 +84,24 @@ RSpec.shared_examples_for 'Dry::Types::Nominal#meta' do
84
84
  end
85
85
  end
86
86
 
87
- describe '#pristine' do
88
- it 'erases meta' do
87
+ describe "#pristine" do
88
+ it "erases meta" do
89
89
  expect(type.meta(foo: :bar).pristine).to eql(type)
90
90
  end
91
91
  end
92
92
  end
93
93
 
94
94
  RSpec.shared_examples_for Dry::Types::Nominal do
95
- it_behaves_like 'Dry::Types::Nominal without primitive'
95
+ it_behaves_like "Dry::Types::Nominal without primitive"
96
96
 
97
- describe '#primitive' do
98
- it 'returns a class' do
97
+ describe "#primitive" do
98
+ it "returns a class" do
99
99
  expect(type.primitive).to be_instance_of(Class)
100
100
  end
101
101
  end
102
102
 
103
- describe '#constructor' do
104
- it 'returns a constructor' do
103
+ describe "#constructor" do
104
+ it "returns a constructor" do
105
105
  constructor = type.constructor(&:to_s)
106
106
 
107
107
  expect(constructor).to be_a(Dry::Types::Type)
@@ -109,40 +109,52 @@ 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 |options = { inputs: Object.new }|
112
+ RSpec.shared_examples_for "a constrained type" do |options = {inputs: Object.new}|
113
113
  inputs = options[:inputs]
114
114
 
115
115
  let(:fallback) { Object.new }
116
116
 
117
- describe '#call' do
118
- it 'yields a block on failure' do
117
+ describe "#call" do
118
+ it "yields a block on failure" do
119
119
  Array(inputs).each do |input|
120
120
  expect(type.(input) { fallback }).to be(fallback)
121
121
  end
122
122
  end
123
123
 
124
- it 'throws an error on invalid input' do
124
+ it "throws an error on invalid input" do
125
125
  Array(inputs).each do |input|
126
126
  expect { type.(input) }.to raise_error(Dry::Types::CoercionError)
127
127
  end
128
128
  end
129
129
  end
130
+
131
+ describe "#constructor" do
132
+ let(:wrapping_constructor) do
133
+ type.constructor { |input, type| type.(input) { fallback } }
134
+ end
135
+
136
+ it "can be wrapped" do
137
+ Array(inputs).each do |input|
138
+ expect(wrapping_constructor.(input)).to be(fallback)
139
+ end
140
+ end
141
+ end
130
142
  end
131
143
 
132
- RSpec.shared_examples_for 'a nominal type' do |inputs: Object.new|
133
- describe '#call' do
134
- it 'always returns the input back' do
144
+ RSpec.shared_examples_for "a nominal type" do |inputs: Object.new|
145
+ describe "#call" do
146
+ it "always returns the input back" do
135
147
  Array(inputs).each do |input|
136
- expect(type.(input) { fail }).to be(input)
148
+ expect(type.(input) { raise }).to be(input)
137
149
  expect(type.(input)).to be(input)
138
150
  end
139
151
  end
140
152
  end
141
153
  end
142
154
 
143
- RSpec.shared_examples_for 'a composable constructor' do
144
- describe '#constructor' do
145
- it 'has aliases for composition' do
155
+ RSpec.shared_examples_for "a composable constructor" do
156
+ describe "#constructor" do
157
+ it "has aliases for composition" do
146
158
  expect(type.method(:append)).to eql(type.method(:constructor))
147
159
  expect(type.method(:prepend)).to eql(type.method(:constructor))
148
160
  expect(type.method(:<<)).to eql(type.method(:constructor))