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