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