dry-types 0.15.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/lib/dry/types/extensions.rb
CHANGED
@@ -1,31 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/monads/maybe'
|
2
4
|
require 'dry/types/decorator'
|
3
5
|
|
4
6
|
module Dry
|
5
7
|
module Types
|
8
|
+
# Maybe extension provides Maybe types where values are wrapped using `Either` monad
|
9
|
+
#
|
10
|
+
# @api public
|
6
11
|
class Maybe
|
7
12
|
include Type
|
8
13
|
include Dry::Equalizer(:type, :options, inspect: false)
|
9
14
|
include Decorator
|
10
15
|
include Builder
|
16
|
+
include Printable
|
11
17
|
include Dry::Monads::Maybe::Mixin
|
12
18
|
|
13
19
|
# @param [Dry::Monads::Maybe, Object] input
|
20
|
+
#
|
21
|
+
# @return [Dry::Monads::Maybe]
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
def call_unsafe(input = Undefined)
|
25
|
+
case input
|
26
|
+
when Dry::Monads::Maybe
|
27
|
+
input
|
28
|
+
when Undefined
|
29
|
+
None()
|
30
|
+
else
|
31
|
+
Maybe(type.call_unsafe(input))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [Dry::Monads::Maybe, Object] input
|
36
|
+
#
|
14
37
|
# @return [Dry::Monads::Maybe]
|
15
|
-
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
def call_safe(input = Undefined, &block)
|
16
41
|
case input
|
17
42
|
when Dry::Monads::Maybe
|
18
43
|
input
|
19
44
|
when Undefined
|
20
45
|
None()
|
21
46
|
else
|
22
|
-
Maybe(type
|
47
|
+
Maybe(type.call_safe(input, &block))
|
23
48
|
end
|
24
49
|
end
|
25
|
-
alias_method :[], :call
|
26
50
|
|
27
51
|
# @param [Object] input
|
52
|
+
#
|
28
53
|
# @return [Result::Success]
|
54
|
+
#
|
55
|
+
# @api public
|
29
56
|
def try(input = Undefined)
|
30
57
|
res = if input.equal?(Undefined)
|
31
58
|
None()
|
@@ -37,13 +64,19 @@ module Dry
|
|
37
64
|
end
|
38
65
|
|
39
66
|
# @return [true]
|
67
|
+
#
|
68
|
+
# @api public
|
40
69
|
def default?
|
41
70
|
true
|
42
71
|
end
|
43
72
|
|
44
73
|
# @param [Object] value
|
74
|
+
#
|
45
75
|
# @see Dry::Types::Builder#default
|
76
|
+
#
|
46
77
|
# @raise [ArgumentError] if nil provided as default value
|
78
|
+
#
|
79
|
+
# @api public
|
47
80
|
def default(value)
|
48
81
|
if value.nil?
|
49
82
|
raise ArgumentError, "nil cannot be used as a default of a maybe type"
|
@@ -54,18 +87,24 @@ module Dry
|
|
54
87
|
end
|
55
88
|
|
56
89
|
module Builder
|
90
|
+
# Turn a type into a maybe type
|
91
|
+
#
|
57
92
|
# @return [Maybe]
|
93
|
+
#
|
94
|
+
# @api public
|
58
95
|
def maybe
|
59
96
|
Maybe.new(Types['strict.nil'] | self)
|
60
97
|
end
|
61
98
|
end
|
62
99
|
|
100
|
+
# @api private
|
63
101
|
class Printer
|
64
102
|
MAPPING[Maybe] = :visit_maybe
|
65
103
|
|
104
|
+
# @api private
|
66
105
|
def visit_maybe(maybe)
|
67
106
|
visit(maybe.type) do |type|
|
68
|
-
yield "Maybe<#{
|
107
|
+
yield "Maybe<#{type}>"
|
69
108
|
end
|
70
109
|
end
|
71
110
|
end
|
data/lib/dry/types/hash.rb
CHANGED
@@ -1,18 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/types/hash/constructor'
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Types
|
7
|
+
# Hash types can be used to define maps and schemas
|
8
|
+
#
|
9
|
+
# @api public
|
5
10
|
class Hash < Nominal
|
6
11
|
NOT_REQUIRED = { required: false }.freeze
|
7
12
|
|
8
|
-
# @overload
|
13
|
+
# @overload schema(type_map, meta = EMPTY_HASH)
|
9
14
|
# @param [{Symbol => Dry::Types::Nominal}] type_map
|
10
15
|
# @param [Hash] meta
|
11
16
|
# @return [Dry::Types::Schema]
|
17
|
+
#
|
12
18
|
# @overload schema(keys)
|
13
19
|
# @param [Array<Dry::Types::Schema::Key>] key List of schema keys
|
14
20
|
# @param [Hash] meta
|
15
21
|
# @return [Dry::Types::Schema]
|
22
|
+
#
|
23
|
+
# @api public
|
16
24
|
def schema(keys_or_map, meta = EMPTY_HASH)
|
17
25
|
if keys_or_map.is_a?(::Array)
|
18
26
|
keys = keys_or_map
|
@@ -27,7 +35,10 @@ module Dry
|
|
27
35
|
#
|
28
36
|
# @param [Type] key_type
|
29
37
|
# @param [Type] value_type
|
38
|
+
#
|
30
39
|
# @return [Map]
|
40
|
+
#
|
41
|
+
# @api public
|
31
42
|
def map(key_type, value_type)
|
32
43
|
Map.new(
|
33
44
|
primitive,
|
@@ -37,8 +48,7 @@ module Dry
|
|
37
48
|
)
|
38
49
|
end
|
39
50
|
|
40
|
-
# @
|
41
|
-
# @return [Schema]
|
51
|
+
# @api private
|
42
52
|
def weak(*)
|
43
53
|
raise "Support for old hash schemas was removed, please refer to the CHANGELOG "\
|
44
54
|
"on how to proceed with the new API https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md"
|
@@ -49,9 +59,13 @@ module Dry
|
|
49
59
|
alias_method :symbolized, :weak
|
50
60
|
|
51
61
|
# Injects a type transformation function for building schemas
|
62
|
+
#
|
52
63
|
# @param [#call,nil] proc
|
53
64
|
# @param [#call,nil] block
|
65
|
+
#
|
54
66
|
# @return [Hash]
|
67
|
+
#
|
68
|
+
# @api public
|
55
69
|
def with_type_transform(proc = nil, &block)
|
56
70
|
fn = proc || block
|
57
71
|
|
@@ -69,14 +83,19 @@ module Dry
|
|
69
83
|
end
|
70
84
|
|
71
85
|
# Whether the type transforms types of schemas created by {Dry::Types::Hash#schema}
|
86
|
+
#
|
72
87
|
# @return [Boolean]
|
88
|
+
#
|
73
89
|
# @api public
|
74
90
|
def transform_types?
|
75
91
|
!options[:type_transform_fn].nil?
|
76
92
|
end
|
77
93
|
|
78
94
|
# @param meta [Boolean] Whether to dump the meta to the AST
|
95
|
+
#
|
79
96
|
# @return [Array] An AST representation
|
97
|
+
#
|
98
|
+
# @api public
|
80
99
|
def to_ast(meta: true)
|
81
100
|
if RUBY_VERSION >= "2.5"
|
82
101
|
opts = options.slice(:type_transform_fn)
|
@@ -1,7 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/types/constructor'
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Types
|
7
|
+
# Hash type exposes additional APIs for working with schema hashes
|
8
|
+
#
|
9
|
+
# @api public
|
5
10
|
class Hash < Nominal
|
6
11
|
class Constructor < ::Dry::Types::Constructor
|
7
12
|
# @api private
|
@@ -9,8 +14,16 @@ module Dry
|
|
9
14
|
::Dry::Types::Hash::Constructor
|
10
15
|
end
|
11
16
|
|
17
|
+
# @return [Lax]
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
def lax
|
21
|
+
type.lax.constructor(fn, meta: meta)
|
22
|
+
end
|
23
|
+
|
12
24
|
private
|
13
25
|
|
26
|
+
# @api private
|
14
27
|
def composable?(value)
|
15
28
|
super && !value.is_a?(Schema::Key)
|
16
29
|
end
|
data/lib/dry/types/inflector.rb
CHANGED
data/lib/dry/types/json.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dry/types/coercions/json'
|
2
4
|
|
3
5
|
module Dry
|
@@ -22,12 +24,8 @@ module Dry
|
|
22
24
|
self['nominal.decimal'].constructor(Coercions::JSON.method(:to_decimal))
|
23
25
|
end
|
24
26
|
|
25
|
-
register('json.array')
|
26
|
-
self['nominal.array'].safe
|
27
|
-
end
|
27
|
+
register('json.array') { self['array'] }
|
28
28
|
|
29
|
-
register('json.hash')
|
30
|
-
self['nominal.hash'].safe
|
31
|
-
end
|
29
|
+
register('json.hash') { self['hash'] }
|
32
30
|
end
|
33
31
|
end
|
@@ -1,61 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/deprecations'
|
1
4
|
require 'dry/types/decorator'
|
2
5
|
|
3
6
|
module Dry
|
4
7
|
module Types
|
5
|
-
|
8
|
+
# Lax types rescue from type-related errors when constructors fail
|
9
|
+
#
|
10
|
+
# @api public
|
11
|
+
class Lax
|
6
12
|
include Type
|
7
13
|
include Decorator
|
8
14
|
include Builder
|
9
15
|
include Printable
|
10
16
|
include Dry::Equalizer(:type, inspect: false)
|
11
17
|
|
12
|
-
private :options, :
|
18
|
+
private :options, :constructor
|
13
19
|
|
14
20
|
# @param [Object] input
|
21
|
+
#
|
15
22
|
# @return [Object]
|
23
|
+
#
|
24
|
+
# @api public
|
16
25
|
def call(input)
|
17
|
-
|
18
|
-
|
19
|
-
if result.respond_to?(:input)
|
20
|
-
result.input
|
21
|
-
else
|
22
|
-
input
|
23
|
-
end
|
26
|
+
type.call_safe(input) { |output = input| output }
|
24
27
|
end
|
25
28
|
alias_method :[], :call
|
29
|
+
alias_method :call_safe, :call
|
30
|
+
alias_method :call_unsafe, :call
|
26
31
|
|
27
32
|
# @param [Object] input
|
28
33
|
# @param [#call,nil] block
|
34
|
+
#
|
29
35
|
# @yieldparam [Failure] failure
|
30
36
|
# @yieldreturn [Result]
|
37
|
+
#
|
31
38
|
# @return [Result,Logic::Result]
|
39
|
+
#
|
40
|
+
# @api public
|
32
41
|
def try(input, &block)
|
33
42
|
type.try(input, &block)
|
34
|
-
rescue
|
35
|
-
result = failure(input,
|
43
|
+
rescue CoercionError => error
|
44
|
+
result = failure(input, error.message)
|
36
45
|
block ? yield(result) : result
|
37
46
|
end
|
38
47
|
|
39
|
-
# @api public
|
40
|
-
#
|
41
48
|
# @see Nominal#to_ast
|
49
|
+
#
|
50
|
+
# @api public
|
42
51
|
def to_ast(meta: true)
|
43
|
-
[:
|
52
|
+
[:lax, type.to_ast(meta: meta)]
|
44
53
|
end
|
45
54
|
|
55
|
+
# @return [Lax]
|
56
|
+
#
|
46
57
|
# @api public
|
47
|
-
|
48
|
-
def safe
|
58
|
+
def lax
|
49
59
|
self
|
50
60
|
end
|
51
61
|
|
52
62
|
private
|
53
63
|
|
54
64
|
# @param [Object, Dry::Types::Constructor] response
|
65
|
+
#
|
55
66
|
# @return [Boolean]
|
67
|
+
#
|
68
|
+
# @api private
|
56
69
|
def decorate?(response)
|
57
|
-
super || response.
|
70
|
+
super || response.is_a?(constructor_type)
|
58
71
|
end
|
59
72
|
end
|
73
|
+
|
74
|
+
extend ::Dry::Core::Deprecations[:'dry-types']
|
75
|
+
Safe = Lax
|
76
|
+
deprecate_constant(:Safe)
|
60
77
|
end
|
61
78
|
end
|
data/lib/dry/types/map.rb
CHANGED
@@ -1,44 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Types
|
5
|
+
# Homogeneous mapping. It describes a hash with unknown keys that match a certain type.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# type = Dry::Types['hash'].map(
|
9
|
+
# Dry::Types['integer'].constrained(gteq: 1, lteq: 10),
|
10
|
+
# Dry::Types['string']
|
11
|
+
# )
|
12
|
+
#
|
13
|
+
# type.(1 => 'right')
|
14
|
+
# # => {1 => 'right'}
|
15
|
+
#
|
16
|
+
# type.('1' => 'wrong')
|
17
|
+
# # Dry::Types::MapError: "1" violates constraints (type?(Integer, "1") AND gteq?(1, "1") AND lteq?(10, "1") failed)
|
18
|
+
#
|
19
|
+
# type.(11 => 'wrong')
|
20
|
+
# # Dry::Types::MapError: 11 violates constraints (lteq?(10, 11) failed)
|
21
|
+
#
|
22
|
+
# @api public
|
3
23
|
class Map < Nominal
|
4
24
|
def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
|
5
25
|
super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
|
6
|
-
validate_options!
|
7
26
|
end
|
8
27
|
|
9
28
|
# @return [Type]
|
29
|
+
#
|
30
|
+
# @api public
|
10
31
|
def key_type
|
11
32
|
options[:key_type]
|
12
33
|
end
|
13
34
|
|
14
35
|
# @return [Type]
|
36
|
+
#
|
37
|
+
# @api public
|
15
38
|
def value_type
|
16
39
|
options[:value_type]
|
17
40
|
end
|
18
41
|
|
19
42
|
# @return [String]
|
43
|
+
#
|
44
|
+
# @api public
|
20
45
|
def name
|
21
|
-
|
46
|
+
'Map'
|
22
47
|
end
|
23
48
|
|
24
49
|
# @param [Hash] hash
|
50
|
+
#
|
25
51
|
# @return [Hash]
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
def call_unsafe(hash)
|
55
|
+
try(hash) { |failure|
|
56
|
+
raise MapError, failure.error.message
|
57
|
+
}.input
|
30
58
|
end
|
31
|
-
alias_method :[], :call
|
32
59
|
|
33
60
|
# @param [Hash] hash
|
34
|
-
#
|
35
|
-
|
36
|
-
|
61
|
+
#
|
62
|
+
# @return [Hash]
|
63
|
+
#
|
64
|
+
# @api private
|
65
|
+
def call_safe(hash)
|
66
|
+
try(hash) { return yield }.input
|
37
67
|
end
|
38
|
-
alias_method :===, :valid?
|
39
68
|
|
40
69
|
# @param [Hash] hash
|
70
|
+
#
|
41
71
|
# @return [Result]
|
72
|
+
#
|
73
|
+
# @api public
|
42
74
|
def try(hash)
|
43
75
|
result = coerce(hash)
|
44
76
|
return result if result.success? || !block_given?
|
@@ -46,51 +78,53 @@ module Dry
|
|
46
78
|
end
|
47
79
|
|
48
80
|
# @param meta [Boolean] Whether to dump the meta to the AST
|
81
|
+
#
|
49
82
|
# @return [Array] An AST representation
|
83
|
+
#
|
84
|
+
# @api public
|
50
85
|
def to_ast(meta: true)
|
51
86
|
[:map,
|
52
|
-
[key_type.to_ast(meta: true),
|
87
|
+
[key_type.to_ast(meta: true),
|
88
|
+
value_type.to_ast(meta: true),
|
53
89
|
meta ? self.meta : EMPTY_HASH]]
|
54
90
|
end
|
55
91
|
|
56
92
|
# @return [Boolean]
|
93
|
+
#
|
94
|
+
# @api public
|
57
95
|
def constrained?
|
58
96
|
value_type.constrained?
|
59
97
|
end
|
60
98
|
|
61
99
|
private
|
62
100
|
|
101
|
+
# @api private
|
63
102
|
def coerce(input)
|
64
103
|
return failure(
|
65
|
-
input, "#{input.inspect} must be an instance of #{primitive}"
|
104
|
+
input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
|
66
105
|
) unless primitive?(input)
|
67
106
|
|
68
107
|
output, failures = {}, []
|
69
108
|
|
70
|
-
input.each do |k,v|
|
71
|
-
res_k =
|
72
|
-
res_v =
|
109
|
+
input.each do |k, v|
|
110
|
+
res_k = key_type.try(k)
|
111
|
+
res_v = value_type.try(v)
|
112
|
+
|
73
113
|
if res_k.failure?
|
74
|
-
failures <<
|
114
|
+
failures << res_k.error
|
75
115
|
elsif output.key?(res_k.input)
|
76
|
-
failures << "duplicate coerced hash key #{res_k.input.inspect}"
|
116
|
+
failures << CoercionError.new("duplicate coerced hash key #{res_k.input.inspect}")
|
77
117
|
elsif res_v.failure?
|
78
|
-
failures <<
|
118
|
+
failures << res_v.error
|
79
119
|
else
|
80
120
|
output[res_k.input] = res_v.input
|
81
121
|
end
|
82
122
|
end
|
83
123
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
def validate_options!
|
90
|
-
%i(key_type value_type).each do |opt|
|
91
|
-
type = send(opt)
|
92
|
-
next if type.is_a?(Type)
|
93
|
-
raise MapError, ":#{opt} must be a #{Type}, got: #{type.inspect}"
|
124
|
+
if failures.empty?
|
125
|
+
success(output)
|
126
|
+
else
|
127
|
+
failure(input, MultipleError.new(failures))
|
94
128
|
end
|
95
129
|
end
|
96
130
|
end
|