dry-types 0.15.0 → 1.2.0

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