dry-types 0.15.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +547 -161
  3. data/LICENSE +17 -17
  4. data/README.md +15 -13
  5. data/dry-types.gemspec +27 -30
  6. data/lib/dry/types/any.rb +23 -12
  7. data/lib/dry/types/array/constructor.rb +32 -0
  8. data/lib/dry/types/array/member.rb +74 -15
  9. data/lib/dry/types/array.rb +18 -2
  10. data/lib/dry/types/builder.rb +118 -22
  11. data/lib/dry/types/builder_methods.rb +46 -16
  12. data/lib/dry/types/coercions/json.rb +43 -7
  13. data/lib/dry/types/coercions/params.rb +117 -32
  14. data/lib/dry/types/coercions.rb +76 -22
  15. data/lib/dry/types/compiler.rb +44 -21
  16. data/lib/dry/types/constrained/coercible.rb +36 -6
  17. data/lib/dry/types/constrained.rb +79 -31
  18. data/lib/dry/types/constraints.rb +18 -4
  19. data/lib/dry/types/constructor/function.rb +216 -0
  20. data/lib/dry/types/constructor/wrapper.rb +94 -0
  21. data/lib/dry/types/constructor.rb +110 -61
  22. data/lib/dry/types/container.rb +6 -1
  23. data/lib/dry/types/core.rb +34 -11
  24. data/lib/dry/types/decorator.rb +38 -17
  25. data/lib/dry/types/default.rb +61 -16
  26. data/lib/dry/types/enum.rb +36 -20
  27. data/lib/dry/types/errors.rb +74 -8
  28. data/lib/dry/types/extensions/maybe.rb +65 -17
  29. data/lib/dry/types/extensions/monads.rb +29 -0
  30. data/lib/dry/types/extensions.rb +7 -1
  31. data/lib/dry/types/fn_container.rb +6 -1
  32. data/lib/dry/types/hash/constructor.rb +17 -4
  33. data/lib/dry/types/hash.rb +32 -20
  34. data/lib/dry/types/inflector.rb +3 -1
  35. data/lib/dry/types/json.rb +18 -16
  36. data/lib/dry/types/lax.rb +75 -0
  37. data/lib/dry/types/map.rb +70 -32
  38. data/lib/dry/types/meta.rb +51 -0
  39. data/lib/dry/types/module.rb +16 -11
  40. data/lib/dry/types/nominal.rb +113 -22
  41. data/lib/dry/types/options.rb +12 -25
  42. data/lib/dry/types/params.rb +39 -25
  43. data/lib/dry/types/predicate_inferrer.rb +238 -0
  44. data/lib/dry/types/predicate_registry.rb +34 -0
  45. data/lib/dry/types/primitive_inferrer.rb +97 -0
  46. data/lib/dry/types/printable.rb +5 -1
  47. data/lib/dry/types/printer.rb +63 -57
  48. data/lib/dry/types/result.rb +29 -3
  49. data/lib/dry/types/schema/key.rb +62 -36
  50. data/lib/dry/types/schema.rb +201 -91
  51. data/lib/dry/types/spec/types.rb +99 -37
  52. data/lib/dry/types/sum.rb +75 -25
  53. data/lib/dry/types/type.rb +49 -0
  54. data/lib/dry/types/version.rb +3 -1
  55. data/lib/dry/types.rb +106 -48
  56. data/lib/dry-types.rb +3 -1
  57. metadata +55 -78
  58. data/.codeclimate.yml +0 -15
  59. data/.gitignore +0 -10
  60. data/.rspec +0 -2
  61. data/.rubocop.yml +0 -43
  62. data/.travis.yml +0 -28
  63. data/.yardopts +0 -5
  64. data/CONTRIBUTING.md +0 -29
  65. data/Gemfile +0 -23
  66. data/Rakefile +0 -20
  67. data/benchmarks/hash_schemas.rb +0 -51
  68. data/lib/dry/types/safe.rb +0 -61
  69. data/log/.gitkeep +0 -0
@@ -1,12 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Types
3
5
  class Constrained
6
+ # Common coercion-related API for constrained types
7
+ #
8
+ # @api public
4
9
  class Coercible < Constrained
5
- # @param [Object] input
6
- # @param [#call,nil] block
7
- # @yieldparam [Failure] failure
8
- # @yieldreturn [Result]
9
- # @return [Result,nil]
10
+ # @return [Object]
11
+ #
12
+ # @api private
13
+ def call_unsafe(input)
14
+ coerced = type.call_unsafe(input)
15
+ result = rule.(coerced)
16
+
17
+ if result.success?
18
+ coerced
19
+ else
20
+ raise ConstraintError.new(result, input)
21
+ end
22
+ end
23
+
24
+ # @return [Object]
25
+ #
26
+ # @api private
27
+ def call_safe(input)
28
+ coerced = type.call_safe(input) { return yield }
29
+
30
+ if rule[coerced]
31
+ coerced
32
+ else
33
+ yield(coerced)
34
+ end
35
+ end
36
+
37
+ # @see Dry::Types::Constrained#try
38
+ #
39
+ # @api public
10
40
  def try(input, &block)
11
41
  result = type.try(input)
12
42
 
@@ -16,7 +46,7 @@ module Dry
16
46
  if validation.success?
17
47
  result
18
48
  else
19
- failure = failure(result.input, validation)
49
+ failure = failure(result.input, ConstraintError.new(validation, input))
20
50
  block ? yield(failure) : failure
21
51
  end
22
52
  else
@@ -1,94 +1,142 @@
1
- require 'dry/types/decorator'
2
- require 'dry/types/constraints'
3
- require 'dry/types/constrained/coercible'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/types/decorator"
5
+ require "dry/types/constraints"
6
+ require "dry/types/constrained/coercible"
4
7
 
5
8
  module Dry
6
9
  module Types
10
+ # Constrained types apply rules to the input
11
+ #
12
+ # @api public
7
13
  class Constrained
8
14
  include Type
9
15
  include Decorator
10
16
  include Builder
11
17
  include Printable
12
- include Dry::Equalizer(:type, :options, :rule, :meta, inspect: false)
18
+ include Dry::Equalizer(:type, :rule, inspect: false, immutable: true)
13
19
 
14
20
  # @return [Dry::Logic::Rule]
15
21
  attr_reader :rule
16
22
 
17
23
  # @param [Type] type
24
+ #
18
25
  # @param [Hash] options
19
- def initialize(type, options)
26
+ #
27
+ # @api public
28
+ def initialize(type, **options)
20
29
  super
21
30
  @rule = options.fetch(:rule)
22
31
  end
23
32
 
24
- # @param [Object] input
25
33
  # @return [Object]
26
- # @raise [ConstraintError]
27
- def call(input)
28
- try(input) { |result|
34
+ #
35
+ # @api private
36
+ def call_unsafe(input)
37
+ result = rule.(input)
38
+
39
+ if result.success?
40
+ type.call_unsafe(input)
41
+ else
29
42
  raise ConstraintError.new(result, input)
30
- }.input
43
+ end
44
+ end
45
+
46
+ # @return [Object]
47
+ #
48
+ # @api private
49
+ def call_safe(input, &block)
50
+ if rule[input]
51
+ type.call_safe(input, &block)
52
+ else
53
+ yield
54
+ end
31
55
  end
32
- alias_method :[], :call
33
-
34
- # @param [Object] input
35
- # @param [#call,nil] block
36
- # @yieldparam [Failure] failure
37
- # @yieldreturn [Result]
38
- # @return [Logic::Result, Result]
39
- # @return [Object] if block given and try fails
56
+
57
+ # Safe coercion attempt. It is similar to #call with a
58
+ # block given but returns a Result instance with metadata
59
+ # about errors (if any).
60
+ #
61
+ # @overload try(input)
62
+ # @param [Object] input
63
+ # @return [Logic::Result]
64
+ #
65
+ # @overload try(input)
66
+ # @param [Object] input
67
+ # @yieldparam [Failure] failure
68
+ # @yieldreturn [Object]
69
+ # @return [Object]
70
+ #
71
+ # @api public
40
72
  def try(input, &block)
41
73
  result = rule.(input)
42
74
 
43
75
  if result.success?
44
76
  type.try(input, &block)
45
77
  else
46
- failure = failure(input, result)
47
- block ? yield(failure) : failure
78
+ failure = failure(input, ConstraintError.new(result, input))
79
+ block_given? ? yield(failure) : failure
48
80
  end
49
81
  end
50
82
 
51
- # @param [Object] value
52
- # @return [Boolean]
53
- def valid?(value)
54
- rule.(value).success? && type.valid?(value)
55
- end
56
-
57
83
  # @param [Hash] options
58
84
  # The options hash provided to {Types.Rule} and combined
59
85
  # using {&} with previous {#rule}
86
+ #
60
87
  # @return [Constrained]
88
+ #
61
89
  # @see Dry::Logic::Operators#and
90
+ #
91
+ # @api public
62
92
  def constrained(options)
63
93
  with(rule: rule & Types.Rule(options))
64
94
  end
65
95
 
66
96
  # @return [true]
97
+ #
98
+ # @api public
67
99
  def constrained?
68
100
  true
69
101
  end
70
102
 
71
103
  # @param [Object] value
104
+ #
72
105
  # @return [Boolean]
106
+ #
107
+ # @api public
73
108
  def ===(value)
74
109
  valid?(value)
75
110
  end
76
111
 
77
- # @api public
112
+ # Build lax type. Constraints are not applicable to lax types hence unwrapping
78
113
  #
114
+ # @return [Lax]
115
+ # @api public
116
+ def lax
117
+ type.lax
118
+ end
119
+
79
120
  # @see Nominal#to_ast
121
+ # @api public
80
122
  def to_ast(meta: true)
81
- [:constrained, [type.to_ast(meta: meta),
82
- rule.to_ast,
83
- meta ? self.meta : EMPTY_HASH]]
123
+ [:constrained, [type.to_ast(meta: meta), rule.to_ast]]
124
+ end
125
+
126
+ # @api private
127
+ def constructor_type
128
+ type.constructor_type
84
129
  end
85
130
 
86
131
  private
87
132
 
88
133
  # @param [Object] response
134
+ #
89
135
  # @return [Boolean]
136
+ #
137
+ # @api private
90
138
  def decorate?(response)
91
- super || response.kind_of?(Constructor)
139
+ super || response.is_a?(Constructor)
92
140
  end
93
141
  end
94
142
  end
@@ -1,18 +1,32 @@
1
- require 'dry/logic/rule_compiler'
2
- require 'dry/logic/predicates'
3
- require 'dry/logic/rule/predicate'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/logic/rule_compiler"
4
+ require "dry/logic/predicates"
5
+ require "dry/logic/rule/predicate"
4
6
 
5
7
  module Dry
8
+ # Helper methods for constraint types
9
+ #
10
+ # @api public
6
11
  module Types
7
12
  # @param [Hash] options
13
+ #
8
14
  # @return [Dry::Logic::Rule]
15
+ #
16
+ # @api public
9
17
  def self.Rule(options)
10
18
  rule_compiler.(
11
- options.map { |key, val| Logic::Rule::Predicate.new(Logic::Predicates[:"#{key}?"]).curry(val).to_ast }
19
+ options.map { |key, val|
20
+ Logic::Rule::Predicate.build(
21
+ Logic::Predicates[:"#{key}?"]
22
+ ).curry(val).to_ast
23
+ }
12
24
  ).reduce(:and)
13
25
  end
14
26
 
15
27
  # @return [Dry::Logic::RuleCompiler]
28
+ #
29
+ # @api private
16
30
  def self.rule_compiler
17
31
  @rule_compiler ||= Logic::RuleCompiler.new(Logic::Predicates)
18
32
  end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "concurrent/map"
5
+
6
+ module Dry
7
+ module Types
8
+ class Constructor < Nominal
9
+ # Function is used internally by Constructor types
10
+ #
11
+ # @api private
12
+ class Function
13
+ # Wrapper for unsafe coercion functions
14
+ #
15
+ # @api private
16
+ class Safe < Function
17
+ def call(input, &block)
18
+ @fn.(input, &block)
19
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
20
+ CoercionError.handle(e, &block)
21
+ end
22
+ end
23
+
24
+ # Coercion via a method call on a known object
25
+ #
26
+ # @api private
27
+ class MethodCall < Function
28
+ @cache = ::Concurrent::Map.new
29
+
30
+ # Choose or build the base class
31
+ #
32
+ # @return [Function]
33
+ def self.call_class(method, public, safe)
34
+ @cache.fetch_or_store([method, public, safe]) do
35
+ if public
36
+ ::Class.new(PublicCall) do
37
+ include PublicCall.call_interface(method, safe)
38
+
39
+ define_method(:__to_s__) do
40
+ "#<PublicCall for :#{method}>"
41
+ end
42
+ end
43
+ elsif safe
44
+ PrivateCall
45
+ else
46
+ PrivateSafeCall
47
+ end
48
+ end
49
+ end
50
+
51
+ # Coercion with a publicly accessible method call
52
+ #
53
+ # @api private
54
+ class PublicCall < MethodCall
55
+ @interfaces = ::Concurrent::Map.new
56
+
57
+ # Choose or build the interface
58
+ #
59
+ # @return [::Module]
60
+ def self.call_interface(method, safe)
61
+ @interfaces.fetch_or_store([method, safe]) do
62
+ ::Module.new do
63
+ if safe
64
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
65
+ def call(input, &block)
66
+ @target.#{method}(input, &block)
67
+ end
68
+ RUBY
69
+ else
70
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
71
+ def call(input, &block)
72
+ @target.#{method}(input)
73
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => error
74
+ CoercionError.handle(error, &block)
75
+ end
76
+ RUBY
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ # Coercion via a private method call
84
+ #
85
+ # @api private
86
+ class PrivateCall < MethodCall
87
+ def call(input, &block)
88
+ @target.send(@name, input, &block)
89
+ end
90
+ end
91
+
92
+ # Coercion via an unsafe private method call
93
+ #
94
+ # @api private
95
+ class PrivateSafeCall < PrivateCall
96
+ def call(input, &block)
97
+ @target.send(@name, input)
98
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
99
+ CoercionError.handle(e, &block)
100
+ end
101
+ end
102
+
103
+ # @api private
104
+ #
105
+ # @return [MethodCall]
106
+ def self.[](fn, safe)
107
+ public = fn.receiver.respond_to?(fn.name)
108
+ MethodCall.call_class(fn.name, public, safe).new(fn)
109
+ end
110
+
111
+ attr_reader :target, :name
112
+
113
+ def initialize(fn)
114
+ super
115
+ @target = fn.receiver
116
+ @name = fn.name
117
+ end
118
+
119
+ def to_ast
120
+ [:method, target, name]
121
+ end
122
+ end
123
+
124
+ class Wrapper < Function
125
+ # @return [Object]
126
+ def call(input, type, &block)
127
+ @fn.(input, type, &block)
128
+ rescue ::NoMethodError, ::TypeError, ::ArgumentError => e
129
+ CoercionError.handle(e, &block)
130
+ end
131
+ alias_method :[], :call
132
+
133
+ def arity
134
+ 2
135
+ end
136
+ end
137
+
138
+ # Choose or build specialized invokation code for a callable
139
+ #
140
+ # @param [#call] fn
141
+ # @return [Function]
142
+ def self.[](fn)
143
+ raise ::ArgumentError, "Missing constructor block" if fn.nil?
144
+
145
+ if fn.is_a?(Function)
146
+ fn
147
+ elsif fn.respond_to?(:arity) && fn.arity.equal?(2)
148
+ Wrapper.new(fn)
149
+ elsif fn.is_a?(::Method)
150
+ MethodCall[fn, yields_block?(fn)]
151
+ elsif yields_block?(fn)
152
+ new(fn)
153
+ else
154
+ Safe.new(fn)
155
+ end
156
+ end
157
+
158
+ # @return [Boolean]
159
+ def self.yields_block?(fn)
160
+ *, (last_arg,) =
161
+ if fn.respond_to?(:parameters)
162
+ fn.parameters
163
+ else
164
+ fn.method(:call).parameters
165
+ end
166
+
167
+ last_arg.equal?(:block)
168
+ end
169
+
170
+ include ::Dry::Equalizer(:fn, immutable: true)
171
+
172
+ attr_reader :fn
173
+
174
+ def initialize(fn)
175
+ @fn = fn
176
+ end
177
+
178
+ # @return [Object]
179
+ def call(input, &block)
180
+ @fn.(input, &block)
181
+ end
182
+ alias_method :[], :call
183
+
184
+ # @return [Integer]
185
+ def arity
186
+ 1
187
+ end
188
+
189
+ def wrapper?
190
+ arity.equal?(2)
191
+ end
192
+
193
+ # @return [Array]
194
+ def to_ast
195
+ if fn.is_a?(::Proc)
196
+ [:id, FnContainer.register(fn)]
197
+ else
198
+ [:callable, fn]
199
+ end
200
+ end
201
+
202
+ # @return [Function]
203
+ def >>(other)
204
+ f = Function[other]
205
+ Function[-> x, &b { f.(self.(x, &b), &b) }]
206
+ end
207
+
208
+ # @return [Function]
209
+ def <<(other)
210
+ f = Function[other]
211
+ Function[-> x, &b { self.(f.(x, &b), &b) }]
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # @api public
6
+ class Constructor < Nominal
7
+ module Wrapper
8
+ # @return [Object]
9
+ #
10
+ # @api private
11
+ def call_safe(input, &block)
12
+ fn.(input, type, &block)
13
+ end
14
+
15
+ # @return [Object]
16
+ #
17
+ # @api private
18
+ def call_unsafe(input)
19
+ fn.(input, type)
20
+ end
21
+
22
+ # @param [Object] input
23
+ # @param [#call,nil] block
24
+ #
25
+ # @return [Logic::Result, Types::Result]
26
+ # @return [Object] if block given and try fails
27
+ #
28
+ # @api public
29
+ def try(input, &block)
30
+ value = fn.(input, type)
31
+ rescue CoercionError => e
32
+ failure = failure(input, e)
33
+ block_given? ? yield(failure) : failure
34
+ else
35
+ type.try(value, &block)
36
+ end
37
+
38
+ # Define a constructor for the type
39
+ #
40
+ # @param [#call,nil] constructor
41
+ # @param [Hash] options
42
+ # @param [#call,nil] block
43
+ #
44
+ # @return [Constructor]
45
+ #
46
+ # @api public
47
+ define_method(:constructor, Builder.instance_method(:constructor))
48
+ alias_method :append, :constructor
49
+ alias_method :>>, :constructor
50
+
51
+ # Build a new constructor by prepending a block to the coercion function
52
+ #
53
+ # @param [#call, nil] new_fn
54
+ # @param [Hash] options
55
+ # @param [#call, nil] block
56
+ #
57
+ # @return [Constructor]
58
+ #
59
+ # @api public
60
+ def prepend(new_fn = nil, **options, &block)
61
+ prep_fn = Function[new_fn || block]
62
+
63
+ decorated =
64
+ if prep_fn.wrapper?
65
+ type.constructor(prep_fn, **options)
66
+ else
67
+ type.prepend(prep_fn, **options)
68
+ end
69
+
70
+ __new__(decorated)
71
+ end
72
+ alias_method :<<, :prepend
73
+
74
+ # @return [Constructor]
75
+ #
76
+ # @api public
77
+ def lax
78
+ # return self back because wrapping function
79
+ # can handle failed type check
80
+ self
81
+ end
82
+
83
+ private
84
+
85
+ # Replace underlying type
86
+ #
87
+ # @api private
88
+ def __new__(type)
89
+ self.class.new(type, *@__args__.drop(1), **@options)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end