dry-types 0.15.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  3. data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
  4. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +18 -2
  7. data/.travis.yml +10 -5
  8. data/.yardopts +6 -2
  9. data/CHANGELOG.md +186 -3
  10. data/Gemfile +11 -5
  11. data/README.md +4 -3
  12. data/Rakefile +4 -2
  13. data/benchmarks/hash_schemas.rb +10 -6
  14. data/benchmarks/lax_schema.rb +15 -0
  15. data/benchmarks/profile_invalid_input.rb +15 -0
  16. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  17. data/benchmarks/profile_valid_input.rb +15 -0
  18. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  19. data/benchmarks/setup.rb +17 -0
  20. data/docsite/source/array-with-member.html.md +13 -0
  21. data/docsite/source/built-in-types.html.md +116 -0
  22. data/docsite/source/constraints.html.md +31 -0
  23. data/docsite/source/custom-types.html.md +93 -0
  24. data/docsite/source/default-values.html.md +91 -0
  25. data/docsite/source/enum.html.md +69 -0
  26. data/docsite/source/getting-started.html.md +57 -0
  27. data/docsite/source/hash-schemas.html.md +169 -0
  28. data/docsite/source/index.html.md +155 -0
  29. data/docsite/source/map.html.md +17 -0
  30. data/docsite/source/optional-values.html.md +96 -0
  31. data/docsite/source/sum.html.md +21 -0
  32. data/dry-types.gemspec +21 -19
  33. data/lib/dry-types.rb +2 -0
  34. data/lib/dry/types.rb +60 -17
  35. data/lib/dry/types/any.rb +21 -10
  36. data/lib/dry/types/array.rb +17 -1
  37. data/lib/dry/types/array/constructor.rb +32 -0
  38. data/lib/dry/types/array/member.rb +72 -13
  39. data/lib/dry/types/builder.rb +49 -5
  40. data/lib/dry/types/builder_methods.rb +43 -16
  41. data/lib/dry/types/coercions.rb +84 -19
  42. data/lib/dry/types/coercions/json.rb +22 -3
  43. data/lib/dry/types/coercions/params.rb +98 -30
  44. data/lib/dry/types/compiler.rb +35 -12
  45. data/lib/dry/types/constrained.rb +78 -27
  46. data/lib/dry/types/constrained/coercible.rb +36 -6
  47. data/lib/dry/types/constraints.rb +15 -1
  48. data/lib/dry/types/constructor.rb +77 -62
  49. data/lib/dry/types/constructor/function.rb +200 -0
  50. data/lib/dry/types/container.rb +5 -0
  51. data/lib/dry/types/core.rb +35 -14
  52. data/lib/dry/types/decorator.rb +37 -10
  53. data/lib/dry/types/default.rb +48 -16
  54. data/lib/dry/types/enum.rb +31 -16
  55. data/lib/dry/types/errors.rb +73 -7
  56. data/lib/dry/types/extensions.rb +6 -0
  57. data/lib/dry/types/extensions/maybe.rb +52 -5
  58. data/lib/dry/types/extensions/monads.rb +29 -0
  59. data/lib/dry/types/fn_container.rb +5 -0
  60. data/lib/dry/types/hash.rb +32 -14
  61. data/lib/dry/types/hash/constructor.rb +16 -3
  62. data/lib/dry/types/inflector.rb +2 -0
  63. data/lib/dry/types/json.rb +7 -5
  64. data/lib/dry/types/{safe.rb → lax.rb} +33 -16
  65. data/lib/dry/types/map.rb +70 -32
  66. data/lib/dry/types/meta.rb +51 -0
  67. data/lib/dry/types/module.rb +10 -5
  68. data/lib/dry/types/nominal.rb +105 -14
  69. data/lib/dry/types/options.rb +12 -25
  70. data/lib/dry/types/params.rb +14 -3
  71. data/lib/dry/types/predicate_inferrer.rb +197 -0
  72. data/lib/dry/types/predicate_registry.rb +34 -0
  73. data/lib/dry/types/primitive_inferrer.rb +97 -0
  74. data/lib/dry/types/printable.rb +5 -1
  75. data/lib/dry/types/printer.rb +70 -64
  76. data/lib/dry/types/result.rb +26 -0
  77. data/lib/dry/types/schema.rb +177 -80
  78. data/lib/dry/types/schema/key.rb +48 -35
  79. data/lib/dry/types/spec/types.rb +43 -6
  80. data/lib/dry/types/sum.rb +70 -21
  81. data/lib/dry/types/type.rb +49 -0
  82. data/lib/dry/types/version.rb +3 -1
  83. metadata +91 -62
@@ -1,7 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/equalizer'
4
+ require 'dry/core/deprecations'
2
5
 
3
6
  module Dry
4
7
  module Types
8
+ # Schema is a hash with explicit member types defined
9
+ #
10
+ # @api public
5
11
  class Schema < Hash
6
12
  # Proxy type for schema keys. Contains only key name and
7
13
  # whether it's required or not. All other calls deletaged
@@ -9,6 +15,7 @@ module Dry
9
15
  #
10
16
  # @see Dry::Types::Schema
11
17
  class Key
18
+ extend ::Dry::Core::Deprecations[:'dry-types']
12
19
  include Type
13
20
  include Dry::Equalizer(:name, :type, :options, inspect: false)
14
21
  include Decorator
@@ -28,12 +35,19 @@ module Dry
28
35
  @name = name
29
36
  end
30
37
 
31
- # @see Dry::Types::Nominal#call
32
- def call(input, &block)
33
- type.(input, &block)
38
+ # @api private
39
+ def call_safe(input, &block)
40
+ type.call_safe(input, &block)
41
+ end
42
+
43
+ # @api private
44
+ def call_unsafe(input)
45
+ type.call_unsafe(input)
34
46
  end
35
47
 
36
48
  # @see Dry::Types::Nominal#try
49
+ #
50
+ # @api public
37
51
  def try(input, &block)
38
52
  type.try(input, &block)
39
53
  end
@@ -41,6 +55,8 @@ module Dry
41
55
  # Whether the key is required in schema input
42
56
  #
43
57
  # @return [Boolean]
58
+ #
59
+ # @api public
44
60
  def required?
45
61
  options.fetch(:required)
46
62
  end
@@ -55,6 +71,8 @@ module Dry
55
71
  #
56
72
  # @param [Boolean] required New value
57
73
  # @return [Dry::Types::Schema::Key]
74
+ #
75
+ # @api public
58
76
  def required(required = Undefined)
59
77
  if Undefined.equal?(required)
60
78
  options.fetch(:required)
@@ -66,36 +84,26 @@ module Dry
66
84
  # Make key not required
67
85
  #
68
86
  # @return [Dry::Types::Schema::Key]
87
+ #
88
+ # @api public
69
89
  def omittable
70
90
  required(false)
71
91
  end
72
92
 
73
- # Construct a default type. Default values are
74
- # evaluated/applied when key is absent in schema
75
- # input.
93
+ # Turn key into a lax type. Lax types are not strict hence such keys are not required
76
94
  #
77
- # @see Dry::Types::Default
78
- # @return [Dry::Types::Schema::Key]
79
- def default(input = Undefined, &block)
80
- new(type.default(input, &block))
81
- end
82
-
83
- # Replace the underlying type
84
- # @param [Dry::Types::Type] type
85
- # @return [Dry::Types::Schema::Key]
86
- def new(type)
87
- self.class.new(type, name, options)
88
- end
89
-
90
- # @see Dry::Types::Safe
91
- # @return [Dry::Types::Schema::Key]
92
- def safe
93
- new(type.safe)
95
+ # @return [Lax]
96
+ #
97
+ # @api public
98
+ def lax
99
+ __new__(type.lax).required(false)
94
100
  end
95
101
 
96
102
  # Dump to internal AST representation
97
103
  #
98
104
  # @return [Array]
105
+ #
106
+ # @api public
99
107
  def to_ast(meta: true)
100
108
  [
101
109
  :key,
@@ -107,23 +115,28 @@ module Dry
107
115
  ]
108
116
  end
109
117
 
110
- # Get/set type metadata. The Key type doesn't have
111
- # its out meta, it delegates these calls to the underlying
112
- # type.
118
+ # @see Dry::Types::Meta#meta
113
119
  #
114
- # @overload meta
115
- # @return [Hash] metadata associated with type
116
- #
117
- # @overload meta(data)
118
- # @param [Hash] new metadata to merge into existing metadata
119
- # @return [Type] new type with added metadata
120
+ # @api public
120
121
  def meta(data = nil)
121
- if data.nil?
122
- type.meta
122
+ if data.nil? || !data.key?(:omittable)
123
+ super
123
124
  else
124
- new(type.meta(data))
125
+ self.class.warn(
126
+ 'Using meta for making schema keys is deprecated, ' \
127
+ 'please use .omittable or .required(false) instead' \
128
+ "\n" + Core::Deprecations::STACK.()
129
+ )
130
+ super.required(!data[:omittable])
125
131
  end
126
132
  end
133
+
134
+ private
135
+
136
+ # @api private
137
+ def decorate?(response)
138
+ response.is_a?(Type)
139
+ end
127
140
  end
128
141
  end
129
142
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
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
8
  describe '#constrained?' do
@@ -41,9 +43,15 @@ RSpec.shared_examples_for 'Dry::Types::Nominal without primitive' do
41
43
 
42
44
  describe '#to_s' do
43
45
  it 'returns a custom string representation' do
44
- if type.class.name.start_with?('Dry::Types')
45
- expect(type.to_s).to start_with('#<Dry::Types')
46
- end
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)
47
55
  end
48
56
  end
49
57
  end
@@ -69,9 +77,9 @@ RSpec.shared_examples_for 'Dry::Types::Nominal#meta' do
69
77
  expect(type.meta).to be_a ::Hash
70
78
  expect(type.meta).to be_frozen
71
79
  expect(type.meta).not_to have_key :immutable_test
72
- derived = type.with(meta: {immutable_test: 1})
80
+ derived = type.meta(immutable_test: 1)
73
81
  expect(derived.meta).to be_frozen
74
- expect(derived.meta).to eql({immutable_test: 1})
82
+ expect(derived.meta).to eql(immutable_test: 1)
75
83
  expect(type.meta).not_to have_key :immutable_test
76
84
  end
77
85
  end
@@ -100,3 +108,32 @@ RSpec.shared_examples_for Dry::Types::Nominal do
100
108
  end
101
109
  end
102
110
  end
111
+
112
+ RSpec.shared_examples_for 'a constrained type' do |inputs: Object.new|
113
+ let(:fallback) { Object.new }
114
+
115
+ describe '#call' do
116
+ it 'yields a block on failure' do
117
+ Array(inputs).each do |input|
118
+ expect(type.(input) { fallback }).to be(fallback)
119
+ end
120
+ end
121
+
122
+ it 'throws an error on invalid input' do
123
+ Array(inputs).each do |input|
124
+ expect { type.(input) }.to raise_error(Dry::Types::CoercionError)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ RSpec.shared_examples_for 'a nominal type' do |inputs: Object.new|
131
+ describe '#call' do
132
+ it 'always returns the input back' do
133
+ Array(inputs).each do |input|
134
+ expect(type.(input) { fail }).to be(input)
135
+ expect(type.(input)).to be(input)
136
+ end
137
+ end
138
+ end
139
+ end
data/lib/dry/types/sum.rb CHANGED
@@ -1,11 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/options'
4
+ require 'dry/types/meta'
2
5
 
3
6
  module Dry
4
7
  module Types
8
+ # Sum type
9
+ #
10
+ # @api public
5
11
  class Sum
6
12
  include Type
7
13
  include Builder
8
14
  include Options
15
+ include Meta
9
16
  include Printable
10
17
  include Dry::Equalizer(:left, :right, :options, :meta, inspect: false)
11
18
 
@@ -15,6 +22,7 @@ module Dry
15
22
  # @return [Type]
16
23
  attr_reader :right
17
24
 
25
+ # @api private
18
26
  class Constrained < Sum
19
27
  # @return [Dry::Logic::Operations::Or]
20
28
  def rule
@@ -25,21 +33,13 @@ module Dry
25
33
  def constrained?
26
34
  true
27
35
  end
28
-
29
- # @param [Object] input
30
- # @return [Object]
31
- # @raise [ConstraintError] if given +input+ not passing {#try}
32
- def call(input)
33
- try(input) { |result|
34
- raise ConstraintError.new(result, input)
35
- }.input
36
- end
37
- alias_method :[], :call
38
36
  end
39
37
 
40
38
  # @param [Type] left
41
39
  # @param [Type] right
42
40
  # @param [Hash] options
41
+ #
42
+ # @api private
43
43
  def initialize(left, right, options = {})
44
44
  super
45
45
  @left, @right = left, right
@@ -47,33 +47,55 @@ module Dry
47
47
  end
48
48
 
49
49
  # @return [String]
50
+ #
51
+ # @api public
50
52
  def name
51
53
  [left, right].map(&:name).join(' | ')
52
54
  end
53
55
 
54
56
  # @return [false]
57
+ #
58
+ # @api public
55
59
  def default?
56
60
  false
57
61
  end
58
62
 
59
63
  # @return [false]
64
+ #
65
+ # @api public
60
66
  def constrained?
61
67
  false
62
68
  end
63
69
 
64
70
  # @return [Boolean]
71
+ #
72
+ # @api public
65
73
  def optional?
66
74
  primitive?(nil)
67
75
  end
68
76
 
69
77
  # @param [Object] input
78
+ #
70
79
  # @return [Object]
71
- def call(input)
72
- try(input).input
80
+ #
81
+ # @api private
82
+ def call_unsafe(input)
83
+ left.call_safe(input) { right.call_unsafe(input) }
73
84
  end
74
- alias_method :[], :call
75
85
 
76
- def try(input, &block)
86
+ # @param [Object] input
87
+ #
88
+ # @return [Object]
89
+ #
90
+ # @api private
91
+ def call_safe(input, &block)
92
+ left.call_safe(input) { right.call_safe(input, &block) }
93
+ end
94
+
95
+ # @param [Object] input
96
+ #
97
+ # @api public
98
+ def try(input)
77
99
  left.try(input) do
78
100
  right.try(input) do |failure|
79
101
  if block_given?
@@ -85,6 +107,7 @@ module Dry
85
107
  end
86
108
  end
87
109
 
110
+ # @api private
88
111
  def success(input)
89
112
  if left.valid?(input)
90
113
  left.success(input)
@@ -95,6 +118,7 @@ module Dry
95
118
  end
96
119
  end
97
120
 
121
+ # @api private
98
122
  def failure(input, _error = nil)
99
123
  if !left.valid?(input)
100
124
  left.failure(input, left.try(input).error)
@@ -104,28 +128,44 @@ module Dry
104
128
  end
105
129
 
106
130
  # @param [Object] value
131
+ #
107
132
  # @return [Boolean]
133
+ #
134
+ # @api private
108
135
  def primitive?(value)
109
136
  left.primitive?(value) || right.primitive?(value)
110
137
  end
111
138
 
112
- # @param [Object] value
113
- # @return [Boolean]
114
- def valid?(value)
115
- left.valid?(value) || right.valid?(value)
139
+ # Manage metadata to the type. If the type is an optional, #meta delegates
140
+ # to the right branch
141
+ #
142
+ # @see [Meta#meta]
143
+ #
144
+ # @api public
145
+ def meta(data = nil)
146
+ if data.nil?
147
+ optional? ? right.meta : super
148
+ elsif optional?
149
+ self.class.new(left, right.meta(data), options)
150
+ else
151
+ super
152
+ end
116
153
  end
117
- alias_method :===, :valid?
118
154
 
119
- # @api public
120
- #
121
155
  # @see Nominal#to_ast
156
+ #
157
+ # @api public
122
158
  def to_ast(meta: true)
123
159
  [:sum, [left.to_ast(meta: meta), right.to_ast(meta: meta), meta ? self.meta : EMPTY_HASH]]
124
160
  end
125
161
 
126
162
  # @param [Hash] options
163
+ #
127
164
  # @return [Constrained,Sum]
165
+ #
128
166
  # @see Builder#constrained
167
+ #
168
+ # @api public
129
169
  def constrained(options)
130
170
  if optional?
131
171
  right.constrained(options).optional
@@ -133,6 +173,15 @@ module Dry
133
173
  super
134
174
  end
135
175
  end
176
+
177
+ # Wrap the type with a proc
178
+ #
179
+ # @return [Proc]
180
+ #
181
+ # @api public
182
+ def to_proc
183
+ proc { |value| self.(value) }
184
+ end
136
185
  end
137
186
  end
138
187
  end
@@ -1,6 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/deprecations'
4
+
1
5
  module Dry
2
6
  module Types
7
+ # Common Type module denoting an object is a Type
8
+ #
9
+ # @api public
3
10
  module Type
11
+ extend ::Dry::Core::Deprecations[:'dry-types']
12
+
13
+ deprecate(:safe, :lax)
14
+
15
+ # Whether a value is a valid member of the type
16
+ #
17
+ # @return [Boolean]
18
+ #
19
+ # @api private
20
+ def valid?(input = Undefined)
21
+ call_safe(input) { return false }
22
+ true
23
+ end
24
+ # Anything can be coerced matches
25
+ alias_method :===, :valid?
26
+
27
+ # Apply type to a value
28
+ #
29
+ # @overload call(input = Undefined)
30
+ # Possibly unsafe coercion attempt. If a value doesn't
31
+ # match the type, an exception will be raised.
32
+ #
33
+ # @param [Object] input
34
+ # @return [Object]
35
+ #
36
+ # @overload call(input = Undefined)
37
+ # When a block is passed, {#call} will never throw an exception on
38
+ # failed coercion, instead it will call the block.
39
+ #
40
+ # @param [Object] input
41
+ # @yieldparam [Object] output Partially coerced value
42
+ # @return [Object]
43
+ #
44
+ # @api public
45
+ def call(input = Undefined, &block)
46
+ if block_given?
47
+ call_safe(input, &block)
48
+ else
49
+ call_unsafe(input)
50
+ end
51
+ end
52
+ alias_method :[], :call
4
53
  end
5
54
  end
6
55
  end