dry-types 0.12.3 → 0.13.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 +9 -6
- data/CHANGELOG.md +92 -3
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +0 -2
- data/Rakefile +4 -2
- data/benchmarks/hash_schemas.rb +2 -2
- data/dry-types.gemspec +2 -3
- data/lib/dry/types.rb +12 -18
- data/lib/dry/types/array.rb +1 -3
- data/lib/dry/types/array/member.rb +6 -2
- data/lib/dry/types/builder.rb +9 -2
- data/lib/dry/types/builder_methods.rb +22 -1
- data/lib/dry/types/coercions/{form.rb → params.rb} +1 -1
- data/lib/dry/types/compat.rb +2 -0
- data/lib/dry/types/compat/form_types.rb +27 -0
- data/lib/dry/types/compat/int.rb +13 -0
- data/lib/dry/types/compiler.rb +30 -7
- data/lib/dry/types/constructor.rb +5 -0
- data/lib/dry/types/core.rb +4 -3
- data/lib/dry/types/decorator.rb +5 -0
- data/lib/dry/types/default.rb +2 -2
- data/lib/dry/types/definition.rb +2 -12
- data/lib/dry/types/enum.rb +42 -14
- data/lib/dry/types/errors.rb +6 -2
- data/lib/dry/types/extensions/maybe.rb +18 -28
- data/lib/dry/types/fn_container.rb +9 -8
- data/lib/dry/types/hash.rb +83 -28
- data/lib/dry/types/hash/schema.rb +98 -179
- data/lib/dry/types/hash/schema_builder.rb +75 -0
- data/lib/dry/types/inflector.rb +7 -0
- data/lib/dry/types/map.rb +93 -0
- data/lib/dry/types/options.rb +3 -3
- data/lib/dry/types/params.rb +53 -0
- data/lib/dry/types/safe.rb +3 -2
- data/lib/dry/types/spec/types.rb +10 -0
- data/lib/dry/types/sum.rb +12 -6
- data/lib/dry/types/version.rb +1 -1
- metadata +29 -37
- data/lib/dry/types/form.rb +0 -53
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'dry/types/fn_container'
|
2
|
+
|
1
3
|
module Dry
|
2
4
|
module Types
|
3
5
|
class Hash < Definition
|
@@ -10,14 +12,27 @@ module Dry
|
|
10
12
|
# @see Dry::Types::Default#evaluate
|
11
13
|
# @see Dry::Types::Default::Callable#evaluate
|
12
14
|
class Schema < Hash
|
15
|
+
NO_TRANSFORM = Dry::Types::FnContainer.register { |x| x }
|
16
|
+
SYMBOLIZE_KEY = Dry::Types::FnContainer.register(:to_sym.to_proc)
|
17
|
+
|
13
18
|
# @return [Hash{Symbol => Definition}]
|
14
19
|
attr_reader :member_types
|
15
20
|
|
21
|
+
# @return [#call]
|
22
|
+
attr_reader :transform_key
|
23
|
+
|
16
24
|
# @param [Class] _primitive
|
17
25
|
# @param [Hash] options
|
18
26
|
# @option options [Hash{Symbol => Definition}] :member_types
|
27
|
+
# @option options [String] :key_transform_fn
|
19
28
|
def initialize(_primitive, options)
|
20
29
|
@member_types = options.fetch(:member_types)
|
30
|
+
|
31
|
+
meta = options[:meta] || EMPTY_HASH
|
32
|
+
key_fn = meta.fetch(:key_transform_fn, NO_TRANSFORM)
|
33
|
+
|
34
|
+
@transform_key = Dry::Types::FnContainer[key_fn]
|
35
|
+
|
21
36
|
super
|
22
37
|
end
|
23
38
|
|
@@ -29,40 +44,46 @@ module Dry
|
|
29
44
|
alias_method :[], :call
|
30
45
|
|
31
46
|
# @param [Hash] hash
|
32
|
-
# @param [#call,nil] block
|
33
47
|
# @yieldparam [Failure] failure
|
34
48
|
# @yieldreturn [Result]
|
35
49
|
# @return [Logic::Result]
|
36
50
|
# @return [Object] if coercion fails and a block is given
|
37
|
-
def try(hash
|
38
|
-
|
39
|
-
|
51
|
+
def try(hash)
|
52
|
+
if hash.is_a?(::Hash)
|
53
|
+
success = true
|
54
|
+
output = {}
|
40
55
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
56
|
+
begin
|
57
|
+
result = try_coerce(hash) do |key, member_result|
|
58
|
+
success &&= member_result.success?
|
59
|
+
output[key] = member_result.input
|
45
60
|
|
46
|
-
|
61
|
+
member_result
|
62
|
+
end
|
63
|
+
rescue ConstraintError, UnknownKeysError, SchemaError => e
|
64
|
+
success = false
|
65
|
+
result = e
|
47
66
|
end
|
48
|
-
|
67
|
+
else
|
49
68
|
success = false
|
50
|
-
|
69
|
+
output = hash
|
70
|
+
result = "#{hash} must be a hash"
|
51
71
|
end
|
52
72
|
|
53
73
|
if success
|
54
74
|
success(output)
|
55
75
|
else
|
56
76
|
failure = failure(output, result)
|
57
|
-
|
77
|
+
block_given? ? yield(failure) : failure
|
58
78
|
end
|
59
79
|
end
|
60
80
|
|
81
|
+
# @param meta [Boolean] Whether to dump the meta to the AST
|
82
|
+
# @return [Array] An AST representation
|
61
83
|
def to_ast(meta: true)
|
62
84
|
[
|
63
|
-
:
|
85
|
+
:hash_schema,
|
64
86
|
[
|
65
|
-
hash_type,
|
66
87
|
member_types.map { |name, member| [:member, [name, member.to_ast(meta: meta)]] },
|
67
88
|
meta ? self.meta : EMPTY_HASH
|
68
89
|
]
|
@@ -77,204 +98,102 @@ module Dry
|
|
77
98
|
end
|
78
99
|
alias_method :===, :valid?
|
79
100
|
|
80
|
-
|
81
|
-
|
82
|
-
def
|
83
|
-
:
|
101
|
+
# Whether the schema rejects unknown keys
|
102
|
+
# @return [Boolean]
|
103
|
+
def strict?
|
104
|
+
meta.fetch(:strict, false)
|
84
105
|
end
|
85
106
|
|
86
|
-
#
|
87
|
-
# @return [
|
88
|
-
def
|
89
|
-
|
90
|
-
yield(key, type.try(value))
|
91
|
-
end
|
107
|
+
# Make the schema intolerant to unknown keys
|
108
|
+
# @return [Schema]
|
109
|
+
def strict
|
110
|
+
meta(strict: true)
|
92
111
|
end
|
93
112
|
|
94
|
-
#
|
95
|
-
# @
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
rescue ConstraintError => e
|
101
|
-
raise SchemaError.new(key, value, e.result)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# @param [Hash] hash
|
107
|
-
# @return [Hash{Symbol => Object}]
|
108
|
-
def resolve(hash)
|
109
|
-
result = {}
|
110
|
-
member_types.each do |key, type|
|
111
|
-
if hash.key?(key)
|
112
|
-
result[key] = yield(type, key, hash[key])
|
113
|
-
else
|
114
|
-
resolve_missing_value(result, key, type)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
result
|
118
|
-
end
|
113
|
+
# Injects a key transformation function
|
114
|
+
# @param [#call,nil] proc
|
115
|
+
# @param [#call,nil] block
|
116
|
+
# @return [Schema]
|
117
|
+
def with_key_transform(proc = nil, &block)
|
118
|
+
fn = proc || block
|
119
119
|
|
120
|
-
|
121
|
-
|
122
|
-
# @param [Type] type
|
123
|
-
# @return [Object]
|
124
|
-
# @see Dry::Types::Default#evaluate
|
125
|
-
# @see Dry::Types::Default::Callable#evaluate
|
126
|
-
def resolve_missing_value(result, key, type)
|
127
|
-
if type.default?
|
128
|
-
result[key] = type.evaluate
|
129
|
-
else
|
130
|
-
super
|
120
|
+
if fn.nil?
|
121
|
+
raise ArgumentError, "a block or callable argument is required"
|
131
122
|
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
# Permissive schema raises a {MissingKeyError} if the given key is missing
|
136
|
-
# in provided hash.
|
137
|
-
class Permissive < Schema
|
138
|
-
private
|
139
123
|
|
140
|
-
|
141
|
-
:
|
124
|
+
handle = Dry::Types::FnContainer.register(fn)
|
125
|
+
meta(key_transform_fn: handle)
|
142
126
|
end
|
143
127
|
|
144
|
-
# @param [Symbol]
|
145
|
-
# @
|
146
|
-
def
|
147
|
-
|
128
|
+
# @param [{Symbol => Definition}] type_map
|
129
|
+
# @return [Schema]
|
130
|
+
def schema(type_map)
|
131
|
+
member_types = self.member_types.merge(transform_types(type_map))
|
132
|
+
Schema.new(primitive, **options, member_types: member_types, meta: meta)
|
148
133
|
end
|
149
|
-
end
|
150
134
|
|
151
|
-
# Strict hash will raise errors when keys are missing or value types are incorrect.
|
152
|
-
# Strict schema raises a {UnknownKeysError} if there are any unexpected
|
153
|
-
# keys in given hash, and raises a {MissingKeyError} if any key is missing
|
154
|
-
# in it.
|
155
|
-
# @example
|
156
|
-
# hash = Types::Hash.strict(name: Types::String, age: Types::Coercible::Int)
|
157
|
-
# hash[email: 'jane@doe.org', name: 'Jane', age: 21]
|
158
|
-
# # => Dry::Types::SchemaKeyError: :email is missing in Hash input
|
159
|
-
class Strict < Permissive
|
160
135
|
private
|
161
136
|
|
162
|
-
def hash_type
|
163
|
-
:strict
|
164
|
-
end
|
165
|
-
|
166
|
-
# @param [Hash] hash
|
167
|
-
# @return [Hash{Symbol => Object}]
|
168
|
-
# @raise [UnknownKeysError]
|
169
|
-
# if there any unexpected key in given hash
|
170
|
-
# @raise [MissingKeyError]
|
171
|
-
# if a required key is not present
|
172
|
-
# @raise [SchemaError]
|
173
|
-
# if a value is the wrong type
|
174
137
|
def resolve(hash)
|
175
|
-
|
176
|
-
raise UnknownKeysError.new(*unexpected) unless unexpected.empty?
|
138
|
+
result = {}
|
177
139
|
|
178
|
-
|
179
|
-
|
140
|
+
hash.each do |key, value|
|
141
|
+
k = transform_key.(key)
|
180
142
|
|
181
|
-
|
143
|
+
if member_types.key?(k)
|
144
|
+
result[k] = yield(member_types[k], k, value)
|
145
|
+
elsif strict?
|
146
|
+
raise UnknownKeysError.new(*unexpected_keys(hash.keys))
|
147
|
+
end
|
182
148
|
end
|
183
|
-
end
|
184
|
-
end
|
185
|
-
|
186
|
-
# {StrictWithDefaults} checks that there are no extra keys
|
187
|
-
# (raises {UnknownKeysError} otherwise) and there a no missing keys
|
188
|
-
# without default values given (raises {MissingKeyError} otherwise).
|
189
|
-
# @see Default#evaluate
|
190
|
-
# @see Default::Callable#evaluate
|
191
|
-
class StrictWithDefaults < Strict
|
192
|
-
private
|
193
149
|
|
194
|
-
|
195
|
-
|
196
|
-
end
|
197
|
-
|
198
|
-
# @param [Hash] result
|
199
|
-
# @param [Symbol] key
|
200
|
-
# @param [Type] type
|
201
|
-
# @return [Object]
|
202
|
-
# @see Dry::Types::Default#evaluate
|
203
|
-
# @see Dry::Types::Default::Callable#evaluate
|
204
|
-
def resolve_missing_value(result, key, type)
|
205
|
-
if type.default?
|
206
|
-
result[key] = type.evaluate
|
207
|
-
else
|
208
|
-
super
|
150
|
+
if result.size < member_types.size
|
151
|
+
resolve_missing_keys(result, &Proc.new)
|
209
152
|
end
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
# Weak schema provides safe types for every type given in schema hash
|
214
|
-
# @see Safe
|
215
|
-
class Weak < Schema
|
216
|
-
# @param [Class] primitive
|
217
|
-
# @param [Hash] options
|
218
|
-
# @see #initialize
|
219
|
-
def self.new(primitive, options)
|
220
|
-
member_types = options.
|
221
|
-
fetch(:member_types).
|
222
|
-
each_with_object({}) { |(k, t), res| res[k] = t.safe }
|
223
153
|
|
224
|
-
|
154
|
+
result
|
225
155
|
end
|
226
156
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
else
|
237
|
-
result = failure(value, "#{value} must be a hash")
|
238
|
-
block ? yield(result) : result
|
157
|
+
def resolve_missing_keys(result)
|
158
|
+
member_types.each do |k, type|
|
159
|
+
next if result.key?(k)
|
160
|
+
|
161
|
+
if type.default?
|
162
|
+
result[k] = yield(type, k, Undefined)
|
163
|
+
elsif !type.meta[:omittable]
|
164
|
+
raise MissingKeyError, k
|
165
|
+
end
|
239
166
|
end
|
240
167
|
end
|
241
168
|
|
242
|
-
|
243
|
-
|
244
|
-
def
|
245
|
-
|
169
|
+
# @param keys [Array<Symbol>]
|
170
|
+
# @return [Array<Symbol>]
|
171
|
+
def unexpected_keys(keys)
|
172
|
+
keys.map(&transform_key) - member_types.keys
|
246
173
|
end
|
247
|
-
end
|
248
|
-
|
249
|
-
# {Symbolized} hash will turn string key names into symbols.
|
250
|
-
class Symbolized < Weak
|
251
|
-
private
|
252
174
|
|
253
|
-
|
254
|
-
|
175
|
+
# @param [Hash] hash
|
176
|
+
# @return [Hash{Symbol => Object}]
|
177
|
+
def try_coerce(hash)
|
178
|
+
resolve(hash) do |type, key, value|
|
179
|
+
yield(key, type.try(value))
|
180
|
+
end
|
255
181
|
end
|
256
182
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
if keyname
|
268
|
-
result[key] = yield(type, key, hash[keyname])
|
269
|
-
else
|
270
|
-
resolve_missing_value(result, key, type)
|
183
|
+
# @param [Hash] hash
|
184
|
+
# @return [Hash{Symbol => Object}]
|
185
|
+
def coerce(hash)
|
186
|
+
resolve(hash) do |type, key, value|
|
187
|
+
begin
|
188
|
+
type.call(value)
|
189
|
+
rescue ConstraintError => e
|
190
|
+
raise SchemaError.new(key, value, e.result)
|
191
|
+
rescue TypeError, ArgumentError => e
|
192
|
+
raise SchemaError.new(key, value, e.message)
|
271
193
|
end
|
272
194
|
end
|
273
|
-
result
|
274
195
|
end
|
275
196
|
end
|
276
|
-
|
277
|
-
private_constant(*constants(false))
|
278
197
|
end
|
279
198
|
end
|
280
199
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'dry/types/hash/schema'
|
2
|
+
require 'dry/types/fn_container'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Types
|
6
|
+
class Hash < Definition
|
7
|
+
# A bulder for legacy schemas
|
8
|
+
# @api private
|
9
|
+
class SchemaBuilder
|
10
|
+
NIL_TO_UNDEFINED = -> v { v.nil? ? Undefined : v }
|
11
|
+
OMITTABLE_KEYS = %i(schema weak symbolized).freeze
|
12
|
+
STRICT = %i(strict strict_with_defaults).freeze
|
13
|
+
|
14
|
+
# @param primitive [Type]
|
15
|
+
# @option options [Hash{Symbol => Definition}] :member_types
|
16
|
+
# @option options [Symbol] :hash_type
|
17
|
+
def call(primitive, options)
|
18
|
+
hash_type = options.fetch(:hash_type)
|
19
|
+
member_types = {}
|
20
|
+
|
21
|
+
options.fetch(:member_types).each do |k, t|
|
22
|
+
member_types[k] = build_type(hash_type, t)
|
23
|
+
end
|
24
|
+
|
25
|
+
instantiate(primitive, **options, member_types: member_types)
|
26
|
+
end
|
27
|
+
|
28
|
+
def instantiate(primitive, hash_type: :base, meta: EMPTY_HASH, **options)
|
29
|
+
meta = meta.dup
|
30
|
+
|
31
|
+
meta[:strict] = true if strict?(hash_type)
|
32
|
+
meta[:key_transform_fn] = Schema::SYMBOLIZE_KEY if hash_type == :symbolized
|
33
|
+
|
34
|
+
Schema.new(primitive, **options, meta: meta)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def omittable?(constructor)
|
40
|
+
OMITTABLE_KEYS.include?(constructor)
|
41
|
+
end
|
42
|
+
|
43
|
+
def strict?(constructor)
|
44
|
+
STRICT.include?(constructor)
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_type(constructor, type)
|
48
|
+
type = safe(constructor, type)
|
49
|
+
type = default(constructor, type) if type.default?
|
50
|
+
type = type.meta(omittable: true) if omittable?(constructor)
|
51
|
+
type
|
52
|
+
end
|
53
|
+
|
54
|
+
def safe(constructor, type)
|
55
|
+
if constructor == :weak || constructor == :symbolized
|
56
|
+
type.safe
|
57
|
+
else
|
58
|
+
type
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def default(constructor, type)
|
63
|
+
case constructor
|
64
|
+
when :strict_with_defaults
|
65
|
+
type
|
66
|
+
when :strict
|
67
|
+
type.type
|
68
|
+
else
|
69
|
+
type.constructor(NIL_TO_UNDEFINED)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Dry
|
2
|
+
module Types
|
3
|
+
class Map < Definition
|
4
|
+
def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
|
5
|
+
super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
|
6
|
+
validate_options!
|
7
|
+
end
|
8
|
+
|
9
|
+
# @return [Type]
|
10
|
+
def key_type
|
11
|
+
options[:key_type]
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Type]
|
15
|
+
def value_type
|
16
|
+
options[:value_type]
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [String]
|
20
|
+
def name
|
21
|
+
"Map"
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Hash] hash
|
25
|
+
# @return [Hash]
|
26
|
+
def call(hash)
|
27
|
+
try(hash) do |failure|
|
28
|
+
raise MapError, failure.error.join("\n")
|
29
|
+
end.input
|
30
|
+
end
|
31
|
+
alias_method :[], :call
|
32
|
+
|
33
|
+
# @param [Hash] hash
|
34
|
+
# @return [Boolean]
|
35
|
+
def valid?(hash)
|
36
|
+
coerce(hash).success?
|
37
|
+
end
|
38
|
+
alias_method :===, :valid?
|
39
|
+
|
40
|
+
# @param [Hash] hash
|
41
|
+
# @return [Result]
|
42
|
+
def try(hash)
|
43
|
+
result = coerce(hash)
|
44
|
+
return result if result.success? || !block_given?
|
45
|
+
yield(result)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param meta [Boolean] Whether to dump the meta to the AST
|
49
|
+
# @return [Array] An AST representation
|
50
|
+
def to_ast(meta: true)
|
51
|
+
[:map,
|
52
|
+
[key_type.to_ast(meta: true), value_type.to_ast(meta: true),
|
53
|
+
meta ? self.meta : EMPTY_HASH]]
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def coerce(input)
|
59
|
+
return failure(
|
60
|
+
input, "#{input.inspect} must be an instance of #{primitive}"
|
61
|
+
) unless primitive?(input)
|
62
|
+
|
63
|
+
output, failures = {}, []
|
64
|
+
|
65
|
+
input.each do |k,v|
|
66
|
+
res_k = options[:key_type].try(k)
|
67
|
+
res_v = options[:value_type].try(v)
|
68
|
+
if res_k.failure?
|
69
|
+
failures << "input key #{k.inspect} is invalid: #{res_k.error}"
|
70
|
+
elsif output.key?(res_k.input)
|
71
|
+
failures << "duplicate coerced hash key #{res_k.input.inspect}"
|
72
|
+
elsif res_v.failure?
|
73
|
+
failures << "input value #{v.inspect} for key #{k.inspect} is invalid: #{res_v.error}"
|
74
|
+
else
|
75
|
+
output[res_k.input] = res_v.input
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
return success(output) if failures.empty?
|
80
|
+
|
81
|
+
failure(input, failures)
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_options!
|
85
|
+
%i(key_type value_type).each do |opt|
|
86
|
+
type = send(opt)
|
87
|
+
next if type.is_a?(Type)
|
88
|
+
raise MapError, ":#{opt} must be a #{Type}, got: #{type.inspect}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|