dry-types 0.15.0 → 1.2.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 (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