dry-types 0.15.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +18 -2
  4. data/.travis.yml +4 -5
  5. data/.yardopts +6 -2
  6. data/CHANGELOG.md +69 -1
  7. data/Gemfile +3 -0
  8. data/README.md +2 -1
  9. data/Rakefile +2 -0
  10. data/benchmarks/hash_schemas.rb +2 -0
  11. data/benchmarks/lax_schema.rb +16 -0
  12. data/benchmarks/profile_invalid_input.rb +15 -0
  13. data/benchmarks/profile_lax_schema_valid.rb +16 -0
  14. data/benchmarks/profile_valid_input.rb +15 -0
  15. data/benchmarks/schema_valid_vs_invalid.rb +21 -0
  16. data/benchmarks/setup.rb +17 -0
  17. data/dry-types.gemspec +4 -2
  18. data/lib/dry-types.rb +2 -0
  19. data/lib/dry/types.rb +51 -13
  20. data/lib/dry/types/any.rb +21 -10
  21. data/lib/dry/types/array.rb +11 -1
  22. data/lib/dry/types/array/member.rb +65 -13
  23. data/lib/dry/types/builder.rb +48 -4
  24. data/lib/dry/types/builder_methods.rb +9 -8
  25. data/lib/dry/types/coercions.rb +71 -19
  26. data/lib/dry/types/coercions/json.rb +22 -3
  27. data/lib/dry/types/coercions/params.rb +98 -30
  28. data/lib/dry/types/compiler.rb +35 -12
  29. data/lib/dry/types/constrained.rb +73 -27
  30. data/lib/dry/types/constrained/coercible.rb +36 -6
  31. data/lib/dry/types/constraints.rb +15 -1
  32. data/lib/dry/types/constructor.rb +90 -43
  33. data/lib/dry/types/constructor/function.rb +201 -0
  34. data/lib/dry/types/container.rb +5 -0
  35. data/lib/dry/types/core.rb +7 -5
  36. data/lib/dry/types/decorator.rb +36 -9
  37. data/lib/dry/types/default.rb +48 -16
  38. data/lib/dry/types/enum.rb +30 -16
  39. data/lib/dry/types/errors.rb +73 -7
  40. data/lib/dry/types/extensions.rb +2 -0
  41. data/lib/dry/types/extensions/maybe.rb +43 -4
  42. data/lib/dry/types/fn_container.rb +5 -0
  43. data/lib/dry/types/hash.rb +22 -3
  44. data/lib/dry/types/hash/constructor.rb +13 -0
  45. data/lib/dry/types/inflector.rb +2 -0
  46. data/lib/dry/types/json.rb +4 -6
  47. data/lib/dry/types/{safe.rb → lax.rb} +34 -17
  48. data/lib/dry/types/map.rb +63 -29
  49. data/lib/dry/types/meta.rb +51 -0
  50. data/lib/dry/types/module.rb +7 -2
  51. data/lib/dry/types/nominal.rb +105 -13
  52. data/lib/dry/types/options.rb +12 -25
  53. data/lib/dry/types/params.rb +5 -3
  54. data/lib/dry/types/printable.rb +5 -1
  55. data/lib/dry/types/printer.rb +58 -57
  56. data/lib/dry/types/result.rb +26 -0
  57. data/lib/dry/types/schema.rb +169 -66
  58. data/lib/dry/types/schema/key.rb +34 -39
  59. data/lib/dry/types/spec/types.rb +41 -1
  60. data/lib/dry/types/sum.rb +70 -21
  61. data/lib/dry/types/type.rb +49 -0
  62. data/lib/dry/types/version.rb +3 -1
  63. metadata +14 -12
@@ -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,18 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/logic/rule_compiler'
2
4
  require 'dry/logic/predicates'
3
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
@@ -1,9 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/types/fn_container'
4
+ require 'dry/types/constructor/function'
2
5
 
3
6
  module Dry
4
7
  module Types
8
+ # Constructor types apply a function to the input that is supposed to return
9
+ # a new value. Coercion is a common use case for constructor types.
10
+ #
11
+ # @api public
5
12
  class Constructor < Nominal
6
- include Dry::Equalizer(:type, :options, :meta, inspect: false)
13
+ include Dry::Equalizer(:type, :options, inspect: false)
14
+
15
+ private :meta
7
16
 
8
17
  # @return [#call]
9
18
  attr_reader :fn
@@ -16,124 +25,161 @@ module Dry
16
25
  # @param [Builder, Object] input
17
26
  # @param [Hash] options
18
27
  # @param [#call, nil] block
28
+ #
29
+ # @api public
19
30
  def self.new(input, **options, &block)
20
31
  type = input.is_a?(Builder) ? input : Nominal.new(input)
21
- super(type, **options, &block)
32
+ super(type, **options, fn: Function[options.fetch(:fn, block)])
22
33
  end
23
34
 
35
+ # Instantiate a new constructor type instance
36
+ #
24
37
  # @param [Type] type
38
+ # @param [Function] fn
25
39
  # @param [Hash] options
26
- # @param [#call, nil] block
27
- def initialize(type, **options, &block)
40
+ #
41
+ # @api private
42
+ def initialize(type, fn: nil, **options)
28
43
  @type = type
29
- @fn = options.fetch(:fn, block)
30
-
31
- raise ArgumentError, 'Missing constructor block' if fn.nil?
44
+ @fn = fn
32
45
 
33
46
  super(type, **options, fn: fn)
34
47
  end
35
48
 
49
+ # Return the inner type's primitive
50
+ #
36
51
  # @return [Class]
52
+ #
53
+ # @api public
37
54
  def primitive
38
55
  type.primitive
39
56
  end
40
57
 
58
+ # Return the inner type's name
59
+ #
41
60
  # @return [String]
61
+ #
62
+ # @api public
42
63
  def name
43
64
  type.name
44
65
  end
45
66
 
46
67
  # @return [Boolean]
68
+ #
69
+ # @api public
47
70
  def default?
48
71
  type.default?
49
72
  end
50
73
 
51
- # @param [Object] input
52
74
  # @return [Object]
53
- def call(input)
54
- type[fn[input]]
75
+ #
76
+ # @api private
77
+ def call_safe(input)
78
+ coerced = fn.(input) { return yield }
79
+ type.call_safe(coerced) { |output = coerced| yield(output) }
80
+ end
81
+
82
+ # @return [Object]
83
+ #
84
+ # @api private
85
+ def call_unsafe(input)
86
+ type.call_unsafe(fn.(input))
55
87
  end
56
- alias_method :[], :call
57
88
 
58
89
  # @param [Object] input
59
90
  # @param [#call,nil] block
91
+ #
60
92
  # @return [Logic::Result, Types::Result]
61
93
  # @return [Object] if block given and try fails
94
+ #
95
+ # @api public
62
96
  def try(input, &block)
63
- type.try(fn[input], &block)
64
- rescue TypeError, ArgumentError => e
65
- failure(input, e.message)
97
+ value = fn.(input)
98
+ rescue CoercionError => error
99
+ failure = failure(input, error)
100
+ block_given? ? yield(failure) : failure
101
+ else
102
+ type.try(value, &block)
66
103
  end
67
104
 
105
+ # Build a new constructor by appending a block to the coercion function
106
+ #
68
107
  # @param [#call, nil] new_fn
69
108
  # @param [Hash] options
70
109
  # @param [#call, nil] block
110
+ #
71
111
  # @return [Constructor]
112
+ #
113
+ # @api public
72
114
  def constructor(new_fn = nil, **options, &block)
73
- left = new_fn || block
74
- right = fn
75
-
76
- with(**options, fn: -> input { left[right[input]] })
115
+ with({**options, fn: fn >> (new_fn || block)})
77
116
  end
78
117
  alias_method :append, :constructor
79
118
  alias_method :>>, :constructor
80
119
 
81
- # @param [Object] value
82
- # @return [Boolean]
83
- def valid?(value)
84
- constructed_value = fn[value]
85
- rescue NoMethodError, TypeError, ArgumentError
86
- false
87
- else
88
- type.valid?(constructed_value)
89
- end
90
- alias_method :===, :valid?
91
-
92
120
  # @return [Class]
121
+ #
122
+ # @api private
93
123
  def constrained_type
94
124
  Constrained::Coercible
95
125
  end
96
126
 
97
- # @api public
98
- #
99
127
  # @see Nominal#to_ast
128
+ #
129
+ # @api public
100
130
  def to_ast(meta: true)
101
- [:constructor, [type.to_ast(meta: meta),
102
- register_fn(fn),
103
- meta ? self.meta : EMPTY_HASH]]
131
+ [:constructor, [type.to_ast(meta: meta), fn.to_ast]]
104
132
  end
105
133
 
106
- # @api public
134
+ # Build a new constructor by prepending a block to the coercion function
107
135
  #
108
136
  # @param [#call, nil] new_fn
109
137
  # @param [Hash] options
110
138
  # @param [#call, nil] block
139
+ #
111
140
  # @return [Constructor]
141
+ #
142
+ # @api public
112
143
  def prepend(new_fn = nil, **options, &block)
113
- left = new_fn || block
114
- right = fn
115
-
116
- with(**options, fn: -> input { right[left[input]] })
144
+ with({**options, fn: fn << (new_fn || block)})
117
145
  end
118
146
  alias_method :<<, :prepend
119
147
 
120
- private
148
+ # Build a lax type
149
+ #
150
+ # @return [Lax]
151
+ # @api public
152
+ def lax
153
+ Lax.new(Constructor.new(type.lax, options))
154
+ end
121
155
 
122
- def register_fn(fn)
123
- Dry::Types::FnContainer.register(fn)
156
+ # Wrap the type with a proc
157
+ #
158
+ # @return [Proc]
159
+ #
160
+ # @api public
161
+ def to_proc
162
+ proc { |value| self.(value) }
124
163
  end
125
164
 
165
+ private
166
+
126
167
  # @param [Symbol] meth
127
168
  # @param [Boolean] include_private
128
169
  # @return [Boolean]
170
+ #
171
+ # @api private
129
172
  def respond_to_missing?(meth, include_private = false)
130
173
  super || type.respond_to?(meth)
131
174
  end
132
175
 
133
176
  # Delegates missing methods to {#type}
177
+ #
134
178
  # @param [Symbol] method
135
179
  # @param [Array] args
136
180
  # @param [#call, nil] block
181
+ #
182
+ # @api private
137
183
  def method_missing(method, *args, &block)
138
184
  if type.respond_to?(method)
139
185
  response = type.__send__(method, *args, &block)
@@ -148,8 +194,9 @@ module Dry
148
194
  end
149
195
  end
150
196
 
197
+ # @api private
151
198
  def composable?(value)
152
- value.kind_of?(Builder)
199
+ value.is_a?(Builder)
153
200
  end
154
201
  end
155
202
  end
@@ -0,0 +1,201 @@
1
+
2
+ # frozen_string_literal: true
3
+
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 => error
20
+ CoercionError.handle(error, &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].hash) do
35
+ if public
36
+ ::Class.new(PublicCall) do
37
+ include PublicCall.call_interface(method, safe)
38
+ end
39
+ elsif safe
40
+ PrivateCall
41
+ else
42
+ PrivateSafeCall
43
+ end
44
+ end
45
+ end
46
+
47
+ # Coercion with a publicly accessible method call
48
+ #
49
+ # @api private
50
+ class PublicCall < MethodCall
51
+ @interfaces = ::Concurrent::Map.new
52
+
53
+ # Choose or build the interface
54
+ #
55
+ # @return [::Module]
56
+ def self.call_interface(method, safe)
57
+ @interfaces.fetch_or_store([method, safe].hash) do
58
+ ::Module.new do
59
+ if safe
60
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
61
+ def call(input, &block)
62
+ @target.#{method}(input, &block)
63
+ end
64
+ RUBY
65
+ else
66
+ module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
67
+ def call(input, &block)
68
+ @target.#{method}(input)
69
+ rescue NoMethodError, TypeError, ArgumentError => error
70
+ CoercionError.handle(error, &block)
71
+ end
72
+ RUBY
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ # Coercion via a private method call
80
+ #
81
+ # @api private
82
+ class PrivateCall < MethodCall
83
+ def call(input, &block)
84
+ @target.send(@name, input, &block)
85
+ end
86
+ end
87
+
88
+ # Coercion via an unsafe private method call
89
+ #
90
+ # @api private
91
+ class PrivateSafeCall < PrivateCall
92
+ def call(input, &block)
93
+ @target.send(@name, input)
94
+ rescue NoMethodError, TypeError, ArgumentError => error
95
+ CoercionError.handle(error, &block)
96
+ end
97
+ end
98
+
99
+ # @api private
100
+ #
101
+ # @return [MethodCall]
102
+ def self.[](fn, safe)
103
+ public = fn.receiver.respond_to?(fn.name)
104
+ MethodCall.call_class(fn.name, public, safe).new(fn)
105
+ end
106
+
107
+ attr_reader :target, :name
108
+
109
+ def initialize(fn)
110
+ super
111
+ @target = fn.receiver
112
+ @name = fn.name
113
+ end
114
+
115
+ def to_ast
116
+ [:method, target, name]
117
+ end
118
+ end
119
+
120
+ # Choose or build specialized invokation code for a callable
121
+ #
122
+ # @param [#call] fn
123
+ # @return [Function]
124
+ def self.[](fn)
125
+ raise ArgumentError, 'Missing constructor block' if fn.nil?
126
+
127
+ if fn.is_a?(Function)
128
+ fn
129
+ elsif fn.is_a?(::Method)
130
+ MethodCall[fn, yields_block?(fn)]
131
+ elsif yields_block?(fn)
132
+ new(fn)
133
+ else
134
+ Safe.new(fn)
135
+ end
136
+ end
137
+
138
+ # @return [Boolean]
139
+ def self.yields_block?(fn)
140
+ *, (last_arg,) =
141
+ if fn.respond_to?(:parameters)
142
+ fn.parameters
143
+ else
144
+ fn.method(:call).parameters
145
+ end
146
+
147
+ last_arg.equal?(:block)
148
+ end
149
+
150
+ include Dry::Equalizer(:fn)
151
+
152
+ attr_reader :fn
153
+
154
+ def initialize(fn)
155
+ @fn = fn
156
+ end
157
+
158
+ # @return [Object]
159
+ def call(input, &block)
160
+ @fn.(input, &block)
161
+ end
162
+ alias_method :[], :call
163
+
164
+ # @return [Array]
165
+ def to_ast
166
+ if fn.is_a?(::Proc)
167
+ [:id, Dry::Types::FnContainer.register(fn)]
168
+ else
169
+ [:callable, fn]
170
+ end
171
+ end
172
+
173
+ if RUBY_VERSION >= '2.6'
174
+ # @return [Function]
175
+ def >>(other)
176
+ proc = other.is_a?(::Proc) ? other : other.fn
177
+ Function[@fn >> proc]
178
+ end
179
+
180
+ # @return [Function]
181
+ def <<(other)
182
+ proc = other.is_a?(::Proc) ? other : other.fn
183
+ Function[@fn << proc]
184
+ end
185
+ else
186
+ # @return [Function]
187
+ def >>(other)
188
+ proc = other.is_a?(::Proc) ? other : other.fn
189
+ Function[-> x { proc[@fn[x]] }]
190
+ end
191
+
192
+ # @return [Function]
193
+ def <<(other)
194
+ proc = other.is_a?(::Proc) ? other : other.fn
195
+ Function[-> x { @fn[proc[x]] }]
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end