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.
- checksums.yaml +4 -4
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +34 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +18 -2
- data/.travis.yml +10 -5
- data/.yardopts +6 -2
- data/CHANGELOG.md +186 -3
- data/Gemfile +11 -5
- data/README.md +4 -3
- data/Rakefile +4 -2
- data/benchmarks/hash_schemas.rb +10 -6
- data/benchmarks/lax_schema.rb +15 -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/docsite/source/array-with-member.html.md +13 -0
- data/docsite/source/built-in-types.html.md +116 -0
- data/docsite/source/constraints.html.md +31 -0
- data/docsite/source/custom-types.html.md +93 -0
- data/docsite/source/default-values.html.md +91 -0
- data/docsite/source/enum.html.md +69 -0
- data/docsite/source/getting-started.html.md +57 -0
- data/docsite/source/hash-schemas.html.md +169 -0
- data/docsite/source/index.html.md +155 -0
- data/docsite/source/map.html.md +17 -0
- data/docsite/source/optional-values.html.md +96 -0
- data/docsite/source/sum.html.md +21 -0
- data/dry-types.gemspec +21 -19
- data/lib/dry-types.rb +2 -0
- data/lib/dry/types.rb +60 -17
- data/lib/dry/types/any.rb +21 -10
- data/lib/dry/types/array.rb +17 -1
- data/lib/dry/types/array/constructor.rb +32 -0
- data/lib/dry/types/array/member.rb +72 -13
- data/lib/dry/types/builder.rb +49 -5
- data/lib/dry/types/builder_methods.rb +43 -16
- data/lib/dry/types/coercions.rb +84 -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 +78 -27
- data/lib/dry/types/constrained/coercible.rb +36 -6
- data/lib/dry/types/constraints.rb +15 -1
- data/lib/dry/types/constructor.rb +77 -62
- data/lib/dry/types/constructor/function.rb +200 -0
- data/lib/dry/types/container.rb +5 -0
- data/lib/dry/types/core.rb +35 -14
- data/lib/dry/types/decorator.rb +37 -10
- data/lib/dry/types/default.rb +48 -16
- data/lib/dry/types/enum.rb +31 -16
- data/lib/dry/types/errors.rb +73 -7
- data/lib/dry/types/extensions.rb +6 -0
- data/lib/dry/types/extensions/maybe.rb +52 -5
- data/lib/dry/types/extensions/monads.rb +29 -0
- data/lib/dry/types/fn_container.rb +5 -0
- data/lib/dry/types/hash.rb +32 -14
- data/lib/dry/types/hash/constructor.rb +16 -3
- data/lib/dry/types/inflector.rb +2 -0
- data/lib/dry/types/json.rb +7 -5
- data/lib/dry/types/{safe.rb → lax.rb} +33 -16
- data/lib/dry/types/map.rb +70 -32
- data/lib/dry/types/meta.rb +51 -0
- data/lib/dry/types/module.rb +10 -5
- data/lib/dry/types/nominal.rb +105 -14
- data/lib/dry/types/options.rb +12 -25
- data/lib/dry/types/params.rb +14 -3
- data/lib/dry/types/predicate_inferrer.rb +197 -0
- data/lib/dry/types/predicate_registry.rb +34 -0
- data/lib/dry/types/primitive_inferrer.rb +97 -0
- data/lib/dry/types/printable.rb +5 -1
- data/lib/dry/types/printer.rb +70 -64
- data/lib/dry/types/result.rb +26 -0
- data/lib/dry/types/schema.rb +177 -80
- data/lib/dry/types/schema/key.rb +48 -35
- data/lib/dry/types/spec/types.rb +43 -6
- 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 +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
|
-
# @
|
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,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,
|
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,
|
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
|
-
#
|
27
|
-
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
def initialize(type, fn: nil, **options)
|
28
41
|
@type = type
|
29
|
-
@fn =
|
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 [
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
64
|
-
rescue
|
65
|
-
failure(input, e
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
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.
|
158
|
+
response = type.public_send(method, *args, &block)
|
140
159
|
|
141
|
-
if
|
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
|