dry-types 0.15.0 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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