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
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
|