dry-types 0.8.1 → 0.9.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/.travis.yml +5 -10
- data/CHANGELOG.md +25 -1
- data/Gemfile +1 -3
- data/README.md +1 -1
- data/Rakefile +11 -2
- data/benchmarks/hash_schemas.rb +51 -0
- data/dry-types.gemspec +4 -3
- data/lib/dry/types.rb +10 -5
- data/lib/dry/types/array/member.rb +4 -0
- data/lib/dry/types/builder.rb +6 -7
- data/lib/dry/types/coercions.rb +6 -1
- data/lib/dry/types/coercions/form.rb +4 -4
- data/lib/dry/types/constrained.rb +10 -5
- data/lib/dry/types/constraints.rb +2 -1
- data/lib/dry/types/core.rb +3 -13
- data/lib/dry/types/decorator.rb +2 -2
- data/lib/dry/types/definition.rb +1 -1
- data/lib/dry/types/errors.rb +15 -22
- data/lib/dry/types/extensions.rb +3 -0
- data/lib/dry/types/extensions/maybe.rb +64 -0
- data/lib/dry/types/hash.rb +14 -0
- data/lib/dry/types/hash/schema.rb +95 -40
- data/lib/dry/types/options.rb +0 -2
- data/lib/dry/types/result.rb +26 -3
- data/lib/dry/types/safe.rb +7 -1
- data/lib/dry/types/sum.rb +9 -1
- data/lib/dry/types/version.rb +1 -1
- data/lib/spec/dry/types.rb +56 -0
- metadata +38 -42
- data/benchmarks/basic.rb +0 -57
- data/benchmarks/constrained.rb +0 -37
- data/lib/dry/types/hashify.rb +0 -12
- data/lib/dry/types/maybe.rb +0 -34
- data/lib/dry/types/struct.rb +0 -25
- data/lib/dry/types/struct/class_interface.rb +0 -107
- data/lib/dry/types/value.rb +0 -13
data/lib/dry/types/definition.rb
CHANGED
data/lib/dry/types/errors.rb
CHANGED
@@ -10,44 +10,37 @@ module Dry
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
SchemaKeyError = Class.new(KeyError)
|
14
|
+
private_constant(:SchemaKeyError)
|
15
|
+
|
16
|
+
class MissingKeyError < SchemaKeyError
|
14
17
|
def initialize(key)
|
15
18
|
super(":#{key} is missing in Hash input")
|
16
19
|
end
|
17
20
|
end
|
18
21
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
def initialize(key)
|
23
|
-
super("Attribute :#{key} has already been defined")
|
22
|
+
class UnknownKeysError < SchemaKeyError
|
23
|
+
def initialize(*keys)
|
24
|
+
super("unexpected keys #{keys.inspect} in Hash input")
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
28
|
ConstraintError = Class.new(TypeError) do
|
28
|
-
attr_reader :result
|
29
|
+
attr_reader :result, :input
|
29
30
|
|
30
|
-
def initialize(result)
|
31
|
+
def initialize(result, input)
|
31
32
|
@result = result
|
33
|
+
@input = input
|
34
|
+
|
32
35
|
if result.is_a?(String)
|
33
|
-
super
|
36
|
+
super(result)
|
34
37
|
else
|
35
|
-
super(
|
38
|
+
super(to_s)
|
36
39
|
end
|
37
40
|
end
|
38
41
|
|
39
|
-
def
|
40
|
-
result
|
41
|
-
end
|
42
|
-
|
43
|
-
def failure_message
|
44
|
-
if result.respond_to?(:rule)
|
45
|
-
rule = result.rule
|
46
|
-
args = rule.predicate.args - [rule.predicate.args.last]
|
47
|
-
"#{rule.predicate.id}(#{args.map(&:inspect).join(', ')}) failed"
|
48
|
-
else
|
49
|
-
result.inspect
|
50
|
-
end
|
42
|
+
def to_s
|
43
|
+
"#{input.inspect} violates constraints (#{result} failed)"
|
51
44
|
end
|
52
45
|
end
|
53
46
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'dry/monads/maybe'
|
2
|
+
require 'dry/types/decorator'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Types
|
6
|
+
class Maybe
|
7
|
+
include Dry::Equalizer(:type, :options)
|
8
|
+
include Decorator
|
9
|
+
include Builder
|
10
|
+
include Dry::Monads::Maybe::Mixin
|
11
|
+
|
12
|
+
def call(input)
|
13
|
+
input.is_a?(Dry::Monads::Maybe) ? input : Maybe(type[input])
|
14
|
+
end
|
15
|
+
alias_method :[], :call
|
16
|
+
|
17
|
+
def try(input)
|
18
|
+
Result::Success.new(Maybe(type[input]))
|
19
|
+
end
|
20
|
+
|
21
|
+
def maybe?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def default(value)
|
26
|
+
if value.nil?
|
27
|
+
raise ArgumentError, "nil cannot be used as a default of a maybe type"
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module Builder
|
35
|
+
def maybe
|
36
|
+
Maybe.new(Types['strict.nil'] | self)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Hash
|
41
|
+
module MaybeTypes
|
42
|
+
def resolve_missing_value(result, key, type)
|
43
|
+
if type.respond_to?(:maybe?) && type.maybe?
|
44
|
+
result[key] = type[nil]
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Schema.include MaybeTypes
|
52
|
+
end
|
53
|
+
|
54
|
+
# Register non-coercible maybe types
|
55
|
+
NON_NIL.each_key do |name|
|
56
|
+
register("maybe.strict.#{name}", self["strict.#{name}"].maybe)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Register coercible maybe types
|
60
|
+
COERCIBLE.each_key do |name|
|
61
|
+
register("maybe.coercible.#{name}", self["coercible.#{name}"].maybe)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/dry/types/hash.rb
CHANGED
@@ -19,13 +19,27 @@ module Dry
|
|
19
19
|
schema(type_map, Weak)
|
20
20
|
end
|
21
21
|
|
22
|
+
def permissive(type_map)
|
23
|
+
schema(type_map, Permissive)
|
24
|
+
end
|
25
|
+
|
22
26
|
def strict(type_map)
|
23
27
|
schema(type_map, Strict)
|
24
28
|
end
|
25
29
|
|
30
|
+
def strict_with_defaults(type_map)
|
31
|
+
schema(type_map, StrictWithDefaults)
|
32
|
+
end
|
33
|
+
|
26
34
|
def symbolized(type_map)
|
27
35
|
schema(type_map, Symbolized)
|
28
36
|
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def resolve_missing_value(_result, _key, _type)
|
41
|
+
# noop
|
42
|
+
end
|
29
43
|
end
|
30
44
|
end
|
31
45
|
end
|
@@ -4,27 +4,33 @@ module Dry
|
|
4
4
|
class Schema < Hash
|
5
5
|
attr_reader :member_types
|
6
6
|
|
7
|
-
def initialize(
|
7
|
+
def initialize(_primitive, options)
|
8
8
|
@member_types = options.fetch(:member_types)
|
9
9
|
super
|
10
10
|
end
|
11
11
|
|
12
|
-
def call(hash
|
13
|
-
|
14
|
-
if hash.key?(key)
|
15
|
-
result[key] = type.__send__(meth, hash[key])
|
16
|
-
else
|
17
|
-
resolve_missing_value(result, key, type)
|
18
|
-
end
|
19
|
-
end
|
12
|
+
def call(hash)
|
13
|
+
coerce(hash)
|
20
14
|
end
|
21
15
|
alias_method :[], :call
|
22
16
|
|
23
17
|
def try(hash, &block)
|
24
|
-
|
25
|
-
output
|
18
|
+
success = true
|
19
|
+
output = {}
|
20
|
+
|
21
|
+
begin
|
22
|
+
result = try_coerce(hash) do |key, member_result|
|
23
|
+
success &&= member_result.success?
|
24
|
+
output[key] = member_result.input
|
25
|
+
|
26
|
+
member_result
|
27
|
+
end
|
28
|
+
rescue ConstraintError, UnknownKeysError, SchemaError => e
|
29
|
+
success = false
|
30
|
+
result = e
|
31
|
+
end
|
26
32
|
|
27
|
-
if
|
33
|
+
if success
|
28
34
|
success(output)
|
29
35
|
else
|
30
36
|
failure = failure(output, result)
|
@@ -34,42 +40,87 @@ module Dry
|
|
34
40
|
|
35
41
|
private
|
36
42
|
|
43
|
+
def try_coerce(hash)
|
44
|
+
resolve(hash) do |type, key, value|
|
45
|
+
yield(key, type.try(value))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def coerce(hash)
|
50
|
+
resolve(hash) do |type, key, value|
|
51
|
+
begin
|
52
|
+
type.call(value)
|
53
|
+
rescue ConstraintError
|
54
|
+
raise SchemaError.new(key, value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def resolve(hash)
|
60
|
+
member_types.each_with_object({}) do |(key, type), result|
|
61
|
+
if hash.key?(key)
|
62
|
+
result[key] = yield(type, key, hash[key])
|
63
|
+
else
|
64
|
+
resolve_missing_value(result, key, type)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
37
69
|
def resolve_missing_value(result, key, type)
|
38
70
|
if type.default?
|
39
71
|
result[key] = type.evaluate
|
40
|
-
|
41
|
-
|
72
|
+
else
|
73
|
+
super
|
42
74
|
end
|
43
75
|
end
|
44
76
|
end
|
45
77
|
|
46
|
-
class
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
78
|
+
class Permissive < Schema
|
79
|
+
private
|
80
|
+
|
81
|
+
def resolve_missing_value(_, key, _)
|
82
|
+
raise MissingKeyError, key
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class Strict < Permissive
|
87
|
+
private
|
88
|
+
|
89
|
+
def resolve(hash)
|
90
|
+
unexpected = hash.keys - member_types.keys
|
91
|
+
raise UnknownKeysError.new(*unexpected) unless unexpected.empty?
|
92
|
+
|
93
|
+
super do |member_type, key, value|
|
94
|
+
type = member_type.default? ? member_type.type : member_type
|
95
|
+
|
96
|
+
yield(type, key, value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class StrictWithDefaults < Strict
|
102
|
+
private
|
103
|
+
|
104
|
+
def resolve_missing_value(result, key, type)
|
105
|
+
if type.default?
|
106
|
+
result[key] = type.value
|
107
|
+
else
|
108
|
+
super
|
57
109
|
end
|
58
110
|
end
|
59
|
-
alias_method :[], :call
|
60
111
|
end
|
61
112
|
|
62
113
|
class Weak < Schema
|
63
|
-
def self.new(primitive, options
|
114
|
+
def self.new(primitive, options)
|
64
115
|
member_types = options.
|
65
|
-
fetch(:member_types
|
116
|
+
fetch(:member_types).
|
66
117
|
each_with_object({}) { |(k, t), res| res[k] = t.safe }
|
67
118
|
|
68
119
|
super(primitive, options.merge(member_types: member_types))
|
69
120
|
end
|
70
121
|
|
71
122
|
def try(hash, &block)
|
72
|
-
if hash.
|
123
|
+
if hash.instance_of?(::Hash)
|
73
124
|
super
|
74
125
|
else
|
75
126
|
result = failure(hash, "#{hash} must be a hash")
|
@@ -79,23 +130,27 @@ module Dry
|
|
79
130
|
end
|
80
131
|
|
81
132
|
class Symbolized < Weak
|
82
|
-
|
83
|
-
member_types.each_with_object({}) do |(key, type), result|
|
84
|
-
if hash.key?(key)
|
85
|
-
result[key] = type.__send__(meth, hash[key])
|
86
|
-
else
|
87
|
-
key_name = key.to_s
|
133
|
+
private
|
88
134
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
135
|
+
def resolve(hash)
|
136
|
+
member_types.each_with_object({}) do |(key, type), result|
|
137
|
+
keyname =
|
138
|
+
if hash.key?(key)
|
139
|
+
key
|
140
|
+
elsif hash.key?(string_key = key.to_s)
|
141
|
+
string_key
|
93
142
|
end
|
143
|
+
|
144
|
+
if keyname
|
145
|
+
result[key] = yield(type, key, hash[keyname])
|
146
|
+
else
|
147
|
+
resolve_missing_value(result, key, type)
|
94
148
|
end
|
95
149
|
end
|
96
150
|
end
|
97
|
-
alias_method :[], :call
|
98
151
|
end
|
152
|
+
|
153
|
+
private_constant(*constants(false))
|
99
154
|
end
|
100
155
|
end
|
101
156
|
end
|
data/lib/dry/types/options.rb
CHANGED
data/lib/dry/types/result.rb
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Types
|
3
|
-
|
4
|
-
|
5
|
+
class Result
|
6
|
+
include Dry::Equalizer(:input)
|
7
|
+
|
8
|
+
attr_reader :input
|
9
|
+
|
10
|
+
def initialize(input)
|
11
|
+
@input = input
|
12
|
+
end
|
13
|
+
|
14
|
+
class Success < Result
|
5
15
|
def success?
|
6
16
|
true
|
7
17
|
end
|
@@ -11,7 +21,20 @@ module Dry
|
|
11
21
|
end
|
12
22
|
end
|
13
23
|
|
14
|
-
class Failure <
|
24
|
+
class Failure < Result
|
25
|
+
include Dry::Equalizer(:input, :error)
|
26
|
+
|
27
|
+
attr_reader :error
|
28
|
+
|
29
|
+
def initialize(input, error)
|
30
|
+
super(input)
|
31
|
+
@error = error
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
error.to_s
|
36
|
+
end
|
37
|
+
|
15
38
|
def success?
|
16
39
|
false
|
17
40
|
end
|
data/lib/dry/types/safe.rb
CHANGED
data/lib/dry/types/sum.rb
CHANGED
@@ -15,6 +15,10 @@ module Dry
|
|
15
15
|
def rule
|
16
16
|
left.rule | right.rule
|
17
17
|
end
|
18
|
+
|
19
|
+
def constrained?
|
20
|
+
true
|
21
|
+
end
|
18
22
|
end
|
19
23
|
|
20
24
|
def initialize(left, right, options = {})
|
@@ -35,9 +39,13 @@ module Dry
|
|
35
39
|
false
|
36
40
|
end
|
37
41
|
|
42
|
+
def constrained?
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
38
46
|
def call(input)
|
39
47
|
try(input) do |result|
|
40
|
-
raise ConstraintError,
|
48
|
+
raise ConstraintError.new(result, input)
|
41
49
|
end.input
|
42
50
|
end
|
43
51
|
alias_method :[], :call
|