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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +18 -2
- data/.travis.yml +4 -5
- data/.yardopts +6 -2
- data/CHANGELOG.md +69 -1
- data/Gemfile +3 -0
- data/README.md +2 -1
- data/Rakefile +2 -0
- data/benchmarks/hash_schemas.rb +2 -0
- data/benchmarks/lax_schema.rb +16 -0
- data/benchmarks/profile_invalid_input.rb +15 -0
- data/benchmarks/profile_lax_schema_valid.rb +16 -0
- data/benchmarks/profile_valid_input.rb +15 -0
- data/benchmarks/schema_valid_vs_invalid.rb +21 -0
- data/benchmarks/setup.rb +17 -0
- data/dry-types.gemspec +4 -2
- data/lib/dry-types.rb +2 -0
- data/lib/dry/types.rb +51 -13
- data/lib/dry/types/any.rb +21 -10
- data/lib/dry/types/array.rb +11 -1
- data/lib/dry/types/array/member.rb +65 -13
- data/lib/dry/types/builder.rb +48 -4
- data/lib/dry/types/builder_methods.rb +9 -8
- data/lib/dry/types/coercions.rb +71 -19
- data/lib/dry/types/coercions/json.rb +22 -3
- data/lib/dry/types/coercions/params.rb +98 -30
- data/lib/dry/types/compiler.rb +35 -12
- data/lib/dry/types/constrained.rb +73 -27
- data/lib/dry/types/constrained/coercible.rb +36 -6
- data/lib/dry/types/constraints.rb +15 -1
- data/lib/dry/types/constructor.rb +90 -43
- data/lib/dry/types/constructor/function.rb +201 -0
- data/lib/dry/types/container.rb +5 -0
- data/lib/dry/types/core.rb +7 -5
- data/lib/dry/types/decorator.rb +36 -9
- data/lib/dry/types/default.rb +48 -16
- data/lib/dry/types/enum.rb +30 -16
- data/lib/dry/types/errors.rb +73 -7
- data/lib/dry/types/extensions.rb +2 -0
- data/lib/dry/types/extensions/maybe.rb +43 -4
- data/lib/dry/types/fn_container.rb +5 -0
- data/lib/dry/types/hash.rb +22 -3
- data/lib/dry/types/hash/constructor.rb +13 -0
- data/lib/dry/types/inflector.rb +2 -0
- data/lib/dry/types/json.rb +4 -6
- data/lib/dry/types/{safe.rb → lax.rb} +34 -17
- data/lib/dry/types/map.rb +63 -29
- data/lib/dry/types/meta.rb +51 -0
- data/lib/dry/types/module.rb +7 -2
- data/lib/dry/types/nominal.rb +105 -13
- data/lib/dry/types/options.rb +12 -25
- data/lib/dry/types/params.rb +5 -3
- data/lib/dry/types/printable.rb +5 -1
- data/lib/dry/types/printer.rb +58 -57
- data/lib/dry/types/result.rb +26 -0
- data/lib/dry/types/schema.rb +169 -66
- data/lib/dry/types/schema/key.rb +34 -39
- data/lib/dry/types/spec/types.rb +41 -1
- data/lib/dry/types/sum.rb +70 -21
- data/lib/dry/types/type.rb +49 -0
- data/lib/dry/types/version.rb +3 -1
- 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
|
-
# @
|
6
|
-
#
|
7
|
-
# @
|
8
|
-
|
9
|
-
|
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|
|
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,
|
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,
|
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
|
-
#
|
27
|
-
|
40
|
+
#
|
41
|
+
# @api private
|
42
|
+
def initialize(type, fn: nil, **options)
|
28
43
|
@type = type
|
29
|
-
@fn =
|
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
|
-
|
54
|
-
|
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
|
-
|
64
|
-
rescue
|
65
|
-
failure(input,
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
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.
|
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
|