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,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/core/deprecations"
5
+
6
+ module Dry
7
+ module Types
8
+ # Schema is a hash with explicit member types defined
9
+ #
10
+ # @api public
11
+ class Schema < Hash
12
+ # Proxy type for schema keys. Contains only key name and
13
+ # whether it's required or not. All other calls deletaged
14
+ # to the wrapped type.
15
+ #
16
+ # @see Dry::Types::Schema
17
+ class Key
18
+ extend ::Dry::Core::Deprecations[:'dry-types']
19
+ include Type
20
+ include Dry::Equalizer(:name, :type, :options, inspect: false, immutable: true)
21
+ include Decorator
22
+ include Builder
23
+ include Printable
24
+
25
+ # @return [Symbol]
26
+ attr_reader :name
27
+
28
+ # @api private
29
+ def initialize(type, name, required: Undefined, **options)
30
+ required = Undefined.default(required) do
31
+ type.meta.fetch(:required) { !type.meta.fetch(:omittable, false) }
32
+ end
33
+
34
+ unless name.is_a?(::Symbol)
35
+ raise ArgumentError, "Schemas can only contain symbol keys, #{name.inspect} given"
36
+ end
37
+
38
+ super(type, name, required: required, **options)
39
+ @name = name
40
+ end
41
+
42
+ # @api private
43
+ def call_safe(input, &block)
44
+ type.call_safe(input, &block)
45
+ end
46
+
47
+ # @api private
48
+ def call_unsafe(input)
49
+ type.call_unsafe(input)
50
+ end
51
+
52
+ # @see Dry::Types::Nominal#try
53
+ #
54
+ # @api public
55
+ def try(input, &block)
56
+ type.try(input, &block)
57
+ end
58
+
59
+ # Whether the key is required in schema input
60
+ #
61
+ # @return [Boolean]
62
+ #
63
+ # @api public
64
+ def required?
65
+ options.fetch(:required)
66
+ end
67
+
68
+ # Control whether the key is required
69
+ #
70
+ # @overload required
71
+ # @return [Boolean]
72
+ #
73
+ # @overload required(required)
74
+ # Change key's "requireness"
75
+ #
76
+ # @param [Boolean] required New value
77
+ # @return [Dry::Types::Schema::Key]
78
+ #
79
+ # @api public
80
+ def required(required = Undefined)
81
+ if Undefined.equal?(required)
82
+ options.fetch(:required)
83
+ else
84
+ with(required: required)
85
+ end
86
+ end
87
+
88
+ # Make key not required
89
+ #
90
+ # @return [Dry::Types::Schema::Key]
91
+ #
92
+ # @api public
93
+ def omittable
94
+ required(false)
95
+ end
96
+
97
+ # Turn key into a lax type. Lax types are not strict hence such keys are not required
98
+ #
99
+ # @return [Lax]
100
+ #
101
+ # @api public
102
+ def lax
103
+ __new__(type.lax).required(false)
104
+ end
105
+
106
+ # Make wrapped type optional
107
+ #
108
+ # @return [Key]
109
+ #
110
+ # @api public
111
+ def optional
112
+ __new__(type.optional)
113
+ end
114
+
115
+ # Dump to internal AST representation
116
+ #
117
+ # @return [Array]
118
+ #
119
+ # @api public
120
+ def to_ast(meta: true)
121
+ [
122
+ :key,
123
+ [
124
+ name,
125
+ required,
126
+ type.to_ast(meta: meta)
127
+ ]
128
+ ]
129
+ end
130
+
131
+ # @see Dry::Types::Meta#meta
132
+ #
133
+ # @api public
134
+ def meta(data = Undefined)
135
+ if Undefined.equal?(data) || !data.key?(:omittable)
136
+ super
137
+ else
138
+ self.class.warn(
139
+ "Using meta for making schema keys is deprecated, " \
140
+ "please use .omittable or .required(false) instead" \
141
+ "\n" + Core::Deprecations::STACK.()
142
+ )
143
+ super.required(!data[:omittable])
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ # @api private
150
+ def decorate?(response)
151
+ response.is_a?(Type)
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -1,94 +1,164 @@
1
- RSpec.shared_examples_for 'Dry::Types::Definition without primitive' do
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "Dry::Types::Nominal without primitive" do
2
4
  def be_boolean
3
- satisfy { |x| x == true || x == false }
5
+ satisfy { |x| x == true || x == false }
4
6
  end
5
7
 
6
- describe '#constrained?' do
7
- it 'returns a boolean value' do
8
+ describe "#constrained?" do
9
+ it "returns a boolean value" do
8
10
  expect(type.constrained?).to be_boolean
9
11
  end
10
12
  end
11
13
 
12
- describe '#default?' do
13
- it 'returns a boolean value' do
14
+ describe "#default?" do
15
+ it "returns a boolean value" do
14
16
  expect(type.default?).to be_boolean
15
17
  end
16
18
  end
17
19
 
18
- describe '#valid?' do
19
- it 'returns a boolean value' do
20
+ describe "#valid?" do
21
+ it "returns a boolean value" do
20
22
  expect(type.valid?(1)).to be_boolean
21
23
  end
22
24
  end
23
25
 
24
- describe '#eql?' do
25
- it 'has #eql? defined' do
26
+ describe "#eql?" do
27
+ it "has #eql? defined" do
26
28
  expect(type).to eql(type)
27
29
  end
28
30
  end
29
31
 
30
- describe '#==' do
31
- it 'has #== defined' do
32
+ describe "#==" do
33
+ it "has #== defined" do
32
34
  expect(type).to eq(type)
33
35
  end
34
36
  end
35
37
 
36
- describe '#optional?' do
37
- it 'returns a boolean value' do
38
+ describe "#optional?" do
39
+ it "returns a boolean value" do
38
40
  expect(type.optional?).to be_boolean
39
41
  end
40
42
  end
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")
47
+ end
48
+ end
49
+
50
+ describe "#to_proc" do
51
+ subject(:callable) { type.to_proc }
52
+
53
+ it "converts a type to a proc" do
54
+ expect(callable).to be_a(Proc)
55
+ end
56
+ end
41
57
  end
42
58
 
43
- RSpec.shared_examples_for 'Dry::Types::Definition#meta' do
44
- describe '#meta' do
45
- it 'allows setting meta information' do
46
- 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")
47
63
 
48
64
  expect(with_meta).to be_instance_of(type.class)
49
- expect(with_meta.meta).to eql(foo: :bar, baz: '1')
65
+ expect(with_meta.meta).to eql(foo: :bar, baz: "1")
50
66
  end
51
67
 
52
- it 'equalizes on empty meta' do
68
+ it "equalizes on empty meta" do
53
69
  expect(type).to eql(type.meta({}))
54
70
  end
55
71
 
56
- it 'equalizes on filled meta' do
57
- 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"))
58
74
  end
59
75
 
60
- it 'is locally immutable' do
76
+ it "is locally immutable" do
61
77
  expect(type.meta).to be_a ::Hash
62
78
  expect(type.meta).to be_frozen
63
79
  expect(type.meta).not_to have_key :immutable_test
64
- derived = type.with(meta: {immutable_test: 1})
80
+ derived = type.meta(immutable_test: 1)
65
81
  expect(derived.meta).to be_frozen
66
- expect(derived.meta).to eql({immutable_test: 1})
82
+ expect(derived.meta).to eql(immutable_test: 1)
67
83
  expect(type.meta).not_to have_key :immutable_test
68
84
  end
69
85
  end
70
86
 
71
- describe '#pristine' do
72
- it 'erases meta' do
87
+ describe "#pristine" do
88
+ it "erases meta" do
73
89
  expect(type.meta(foo: :bar).pristine).to eql(type)
74
90
  end
75
91
  end
76
92
  end
77
93
 
78
- RSpec.shared_examples_for Dry::Types::Definition do
79
- it_behaves_like 'Dry::Types::Definition without primitive'
94
+ RSpec.shared_examples_for Dry::Types::Nominal do
95
+ it_behaves_like "Dry::Types::Nominal without primitive"
80
96
 
81
- describe '#primitive' do
82
- it 'returns a class' do
97
+ describe "#primitive" do
98
+ it "returns a class" do
83
99
  expect(type.primitive).to be_instance_of(Class)
84
100
  end
85
101
  end
86
102
 
87
- describe '#constructor' do
88
- it 'returns a constructor' do
103
+ describe "#constructor" do
104
+ it "returns a constructor" do
89
105
  constructor = type.constructor(&:to_s)
90
106
 
91
107
  expect(constructor).to be_a(Dry::Types::Type)
92
108
  end
93
109
  end
94
110
  end
111
+
112
+ RSpec.shared_examples_for "a constrained type" do |options = {inputs: Object.new}|
113
+ inputs = options[:inputs]
114
+
115
+ let(:fallback) { Object.new }
116
+
117
+ describe "#call" do
118
+ it "yields a block on failure" do
119
+ Array(inputs).each do |input|
120
+ expect(type.(input) { fallback }).to be(fallback)
121
+ end
122
+ end
123
+
124
+ it "throws an error on invalid input" do
125
+ Array(inputs).each do |input|
126
+ expect { type.(input) }.to raise_error(Dry::Types::CoercionError)
127
+ end
128
+ end
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
142
+ end
143
+
144
+ RSpec.shared_examples_for "a nominal type" do |inputs: Object.new|
145
+ describe "#call" do
146
+ it "always returns the input back" do
147
+ Array(inputs).each do |input|
148
+ expect(type.(input) { raise }).to be(input)
149
+ expect(type.(input)).to be(input)
150
+ end
151
+ end
152
+ end
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
data/lib/dry/types/sum.rb CHANGED
@@ -1,12 +1,21 @@
1
- require 'dry/types/options'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/types/options"
5
+ require "dry/types/meta"
2
6
 
3
7
  module Dry
4
8
  module Types
9
+ # Sum type
10
+ #
11
+ # @api public
5
12
  class Sum
6
13
  include Type
7
- include Dry::Equalizer(:left, :right, :options, :meta)
8
14
  include Builder
9
15
  include Options
16
+ include Meta
17
+ include Printable
18
+ include Dry::Equalizer(:left, :right, :options, :meta, inspect: false, immutable: true)
10
19
 
11
20
  # @return [Type]
12
21
  attr_reader :left
@@ -14,6 +23,7 @@ module Dry
14
23
  # @return [Type]
15
24
  attr_reader :right
16
25
 
26
+ # @api private
17
27
  class Constrained < Sum
18
28
  # @return [Dry::Logic::Operations::Or]
19
29
  def rule
@@ -24,68 +34,81 @@ module Dry
24
34
  def constrained?
25
35
  true
26
36
  end
27
-
28
- # @param [Object] input
29
- # @return [Object]
30
- # @raise [ConstraintError] if given +input+ not passing {#try}
31
- def call(input)
32
- try(input) do |result|
33
- raise ConstraintError.new(result, input)
34
- end.input
35
- end
36
- alias_method :[], :call
37
37
  end
38
38
 
39
39
  # @param [Type] left
40
40
  # @param [Type] right
41
41
  # @param [Hash] options
42
- def initialize(left, right, options = {})
42
+ #
43
+ # @api private
44
+ def initialize(left, right, **options)
43
45
  super
44
46
  @left, @right = left, right
45
47
  freeze
46
48
  end
47
49
 
48
50
  # @return [String]
51
+ #
52
+ # @api public
49
53
  def name
50
- [left, right].map(&:name).join(' | ')
54
+ [left, right].map(&:name).join(" | ")
51
55
  end
52
56
 
53
57
  # @return [false]
58
+ #
59
+ # @api public
54
60
  def default?
55
61
  false
56
62
  end
57
63
 
58
64
  # @return [false]
65
+ #
66
+ # @api public
59
67
  def constrained?
60
68
  false
61
69
  end
62
70
 
63
71
  # @return [Boolean]
72
+ #
73
+ # @api public
64
74
  def optional?
65
- left == Types['strict.nil']
75
+ primitive?(nil)
66
76
  end
67
77
 
68
78
  # @param [Object] input
79
+ #
69
80
  # @return [Object]
70
- def call(input)
71
- try(input).input
81
+ #
82
+ # @api private
83
+ def call_unsafe(input)
84
+ left.call_safe(input) { right.call_unsafe(input) }
72
85
  end
73
- alias_method :[], :call
74
-
75
- def try(input, &block)
76
- result = left.try(input) do
77
- right.try(input)
78
- end
79
86
 
80
- return result if result.success?
87
+ # @param [Object] input
88
+ #
89
+ # @return [Object]
90
+ #
91
+ # @api private
92
+ def call_safe(input, &block)
93
+ left.call_safe(input) { right.call_safe(input, &block) }
94
+ end
81
95
 
82
- if block
83
- yield(result)
84
- else
85
- result
96
+ # @param [Object] input
97
+ #
98
+ # @api public
99
+ def try(input)
100
+ left.try(input) do
101
+ right.try(input) do |failure|
102
+ if block_given?
103
+ yield(failure)
104
+ else
105
+ failure
106
+ end
107
+ end
86
108
  end
87
109
  end
88
110
 
111
+ # @api private
89
112
  def success(input)
90
113
  if left.valid?(input)
91
114
  left.success(input)
@@ -96,6 +119,7 @@ module Dry
96
119
  end
97
120
  end
98
121
 
122
+ # @api private
99
123
  def failure(input, _error = nil)
100
124
  if !left.valid?(input)
101
125
  left.failure(input, left.try(input).error)
@@ -105,28 +129,44 @@ module Dry
105
129
  end
106
130
 
107
131
  # @param [Object] value
132
+ #
108
133
  # @return [Boolean]
134
+ #
135
+ # @api private
109
136
  def primitive?(value)
110
137
  left.primitive?(value) || right.primitive?(value)
111
138
  end
112
139
 
113
- # @param [Object] value
114
- # @return [Boolean]
115
- def valid?(value)
116
- left.valid?(value) || right.valid?(value)
140
+ # Manage metadata to the type. If the type is an optional, #meta delegates
141
+ # to the right branch
142
+ #
143
+ # @see [Meta#meta]
144
+ #
145
+ # @api public
146
+ def meta(data = Undefined)
147
+ if Undefined.equal?(data)
148
+ optional? ? right.meta : super
149
+ elsif optional?
150
+ self.class.new(left, right.meta(data), **options)
151
+ else
152
+ super
153
+ end
117
154
  end
118
- alias_method :===, :valid?
119
155
 
120
- # @api public
156
+ # @see Nominal#to_ast
121
157
  #
122
- # @see Definition#to_ast
158
+ # @api public
123
159
  def to_ast(meta: true)
124
160
  [:sum, [left.to_ast(meta: meta), right.to_ast(meta: meta), meta ? self.meta : EMPTY_HASH]]
125
161
  end
126
162
 
127
163
  # @param [Hash] options
164
+ #
128
165
  # @return [Constrained,Sum]
166
+ #
129
167
  # @see Builder#constrained
168
+ #
169
+ # @api public
130
170
  def constrained(options)
131
171
  if optional?
132
172
  right.constrained(options).optional
@@ -134,6 +174,15 @@ module Dry
134
174
  super
135
175
  end
136
176
  end
177
+
178
+ # Wrap the type with a proc
179
+ #
180
+ # @return [Proc]
181
+ #
182
+ # @api public
183
+ def to_proc
184
+ proc { |value| self.(value) }
185
+ end
137
186
  end
138
187
  end
139
188
  end