dry-types 1.2.1 → 1.5.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +405 -221
  3. data/LICENSE +1 -1
  4. data/README.md +14 -13
  5. data/dry-types.gemspec +26 -31
  6. data/lib/dry-types.rb +1 -1
  7. data/lib/dry/types.rb +55 -40
  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 +70 -18
  13. data/lib/dry/types/builder_methods.rb +6 -3
  14. data/lib/dry/types/coercions.rb +6 -17
  15. data/lib/dry/types/coercions/json.rb +22 -5
  16. data/lib/dry/types/coercions/params.rb +21 -4
  17. data/lib/dry/types/compiler.rb +10 -10
  18. data/lib/dry/types/constrained.rb +6 -10
  19. data/lib/dry/types/constraints.rb +3 -3
  20. data/lib/dry/types/constructor.rb +40 -7
  21. data/lib/dry/types/constructor/function.rb +47 -32
  22. data/lib/dry/types/constructor/wrapper.rb +94 -0
  23. data/lib/dry/types/container.rb +1 -1
  24. data/lib/dry/types/core.rb +15 -13
  25. data/lib/dry/types/decorator.rb +2 -9
  26. data/lib/dry/types/default.rb +15 -3
  27. data/lib/dry/types/enum.rb +4 -4
  28. data/lib/dry/types/errors.rb +6 -6
  29. data/lib/dry/types/extensions.rb +2 -2
  30. data/lib/dry/types/extensions/maybe.rb +17 -17
  31. data/lib/dry/types/extensions/monads.rb +1 -1
  32. data/lib/dry/types/fn_container.rb +1 -1
  33. data/lib/dry/types/hash.rb +9 -15
  34. data/lib/dry/types/hash/constructor.rb +1 -1
  35. data/lib/dry/types/inflector.rb +1 -1
  36. data/lib/dry/types/json.rb +15 -15
  37. data/lib/dry/types/lax.rb +4 -7
  38. data/lib/dry/types/map.rb +2 -2
  39. data/lib/dry/types/meta.rb +3 -3
  40. data/lib/dry/types/module.rb +6 -6
  41. data/lib/dry/types/nominal.rb +11 -12
  42. data/lib/dry/types/params.rb +31 -28
  43. data/lib/dry/types/predicate_inferrer.rb +52 -11
  44. data/lib/dry/types/predicate_registry.rb +1 -1
  45. data/lib/dry/types/primitive_inferrer.rb +1 -1
  46. data/lib/dry/types/printer.rb +25 -25
  47. data/lib/dry/types/result.rb +3 -3
  48. data/lib/dry/types/schema.rb +26 -13
  49. data/lib/dry/types/schema/key.rb +20 -7
  50. data/lib/dry/types/spec/types.rb +65 -42
  51. data/lib/dry/types/sum.rb +6 -6
  52. data/lib/dry/types/type.rb +1 -1
  53. data/lib/dry/types/version.rb +1 -1
  54. metadata +27 -84
  55. data/.codeclimate.yml +0 -12
  56. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  57. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -34
  58. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  59. data/.github/workflows/custom_ci.yml +0 -76
  60. data/.github/workflows/docsite.yml +0 -34
  61. data/.github/workflows/sync_configs.yml +0 -34
  62. data/.gitignore +0 -11
  63. data/.rspec +0 -4
  64. data/.rubocop.yml +0 -89
  65. data/.yardopts +0 -9
  66. data/CODE_OF_CONDUCT.md +0 -13
  67. data/CONTRIBUTING.md +0 -29
  68. data/Gemfile +0 -32
  69. data/Rakefile +0 -22
  70. data/benchmarks/hash_schemas.rb +0 -55
  71. data/benchmarks/lax_schema.rb +0 -15
  72. data/benchmarks/profile_invalid_input.rb +0 -15
  73. data/benchmarks/profile_lax_schema_valid.rb +0 -16
  74. data/benchmarks/profile_valid_input.rb +0 -15
  75. data/benchmarks/schema_valid_vs_invalid.rb +0 -21
  76. data/benchmarks/setup.rb +0 -17
  77. data/docsite/source/array-with-member.html.md +0 -13
  78. data/docsite/source/built-in-types.html.md +0 -116
  79. data/docsite/source/constraints.html.md +0 -31
  80. data/docsite/source/custom-types.html.md +0 -93
  81. data/docsite/source/default-values.html.md +0 -91
  82. data/docsite/source/enum.html.md +0 -69
  83. data/docsite/source/extensions.html.md +0 -15
  84. data/docsite/source/extensions/maybe.html.md +0 -57
  85. data/docsite/source/extensions/monads.html.md +0 -61
  86. data/docsite/source/getting-started.html.md +0 -57
  87. data/docsite/source/hash-schemas.html.md +0 -169
  88. data/docsite/source/index.html.md +0 -156
  89. data/docsite/source/map.html.md +0 -17
  90. data/docsite/source/optional-values.html.md +0 -35
  91. data/docsite/source/sum.html.md +0 -21
@@ -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
@@ -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
@@ -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
@@ -179,7 +171,7 @@ module Dry
179
171
  with(strict: strict)
180
172
  end
181
173
 
182
- # Injects a key transformation function
174
+ # Inject a key transformation function
183
175
  #
184
176
  # @param [#call,nil] proc
185
177
  # @param [#call,nil] block
@@ -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)
@@ -284,6 +276,27 @@ module Dry
284
276
  Lax.new(schema(keys.map(&:lax)))
285
277
  end
286
278
 
279
+ # Merge given schema keys into current schema
280
+ #
281
+ # A new instance is returned.
282
+ #
283
+ # @param other [Schema] schema
284
+ # @return [Schema]
285
+ #
286
+ # @api public
287
+ def merge(other)
288
+ schema(other.keys)
289
+ end
290
+
291
+ # Empty schema with the same options
292
+ #
293
+ # @return [Schema]
294
+ #
295
+ # @api public
296
+ def clear
297
+ with(keys: EMPTY_ARRAY)
298
+ end
299
+
287
300
  private
288
301
 
289
302
  # @param [Array<Dry::Types::Schema::Keys>] keys
@@ -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
@@ -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,13 +131,13 @@ 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(
126
- 'Using meta for making schema keys is deprecated, ' \
127
- 'please use .omittable or .required(false) instead' \
139
+ "Using meta for making schema keys is deprecated, " \
140
+ "please use .omittable or .required(false) instead" \
128
141
  "\n" + Core::Deprecations::STACK.()
129
142
  )
130
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,33 +109,56 @@ 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
154
+
155
+ RSpec.shared_examples_for "a composable constructor" do
156
+ describe "#constructor" do
157
+ it "has aliases for composition" do
158
+ expect(type.method(:append)).to eql(type.method(:constructor))
159
+ expect(type.method(:prepend)).to eql(type.method(:constructor))
160
+ expect(type.method(:<<)).to eql(type.method(:constructor))
161
+ expect(type.method(:>>)).to eql(type.method(:constructor))
162
+ end
163
+ end
164
+ end