dry-types 0.14.1 → 0.15.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/.codeclimate.yml +15 -0
- data/.rubocop.yml +43 -0
- data/.travis.yml +12 -11
- data/CHANGELOG.md +115 -4
- data/Gemfile +2 -3
- data/benchmarks/hash_schemas.rb +5 -5
- data/dry-types.gemspec +1 -1
- data/lib/dry/types.rb +67 -45
- data/lib/dry/types/any.rb +11 -2
- data/lib/dry/types/array.rb +1 -4
- data/lib/dry/types/array/member.rb +2 -2
- data/lib/dry/types/builder.rb +23 -3
- data/lib/dry/types/builder_methods.rb +10 -11
- data/lib/dry/types/coercions/params.rb +2 -0
- data/lib/dry/types/compat.rb +0 -2
- data/lib/dry/types/compiler.rb +25 -33
- data/lib/dry/types/constrained.rb +5 -4
- data/lib/dry/types/constructor.rb +32 -11
- data/lib/dry/types/container.rb +2 -0
- data/lib/dry/types/core.rb +22 -12
- data/lib/dry/types/default.rb +4 -4
- data/lib/dry/types/enum.rb +10 -3
- data/lib/dry/types/errors.rb +1 -1
- data/lib/dry/types/extensions/maybe.rb +11 -1
- data/lib/dry/types/hash.rb +70 -63
- data/lib/dry/types/hash/constructor.rb +20 -0
- data/lib/dry/types/json.rb +7 -7
- data/lib/dry/types/map.rb +6 -1
- data/lib/dry/types/module.rb +115 -0
- data/lib/dry/types/{definition.rb → nominal.rb} +10 -4
- data/lib/dry/types/options.rb +2 -2
- data/lib/dry/types/params.rb +11 -11
- data/lib/dry/types/printable.rb +12 -0
- data/lib/dry/types/printer.rb +309 -0
- data/lib/dry/types/result.rb +2 -2
- data/lib/dry/types/safe.rb +4 -2
- data/lib/dry/types/schema.rb +298 -0
- data/lib/dry/types/schema/key.rb +130 -0
- data/lib/dry/types/spec/types.rb +12 -4
- data/lib/dry/types/sum.rb +14 -15
- data/lib/dry/types/version.rb +1 -1
- metadata +18 -8
- data/lib/dry/types/compat/form_types.rb +0 -27
- data/lib/dry/types/compat/int.rb +0 -14
- data/lib/dry/types/hash/schema.rb +0 -199
- data/lib/dry/types/hash/schema_builder.rb +0 -75
data/lib/dry/types/safe.rb
CHANGED
@@ -4,9 +4,11 @@ module Dry
|
|
4
4
|
module Types
|
5
5
|
class Safe
|
6
6
|
include Type
|
7
|
-
include Dry::Equalizer(:type)
|
8
7
|
include Decorator
|
9
8
|
include Builder
|
9
|
+
include Printable
|
10
|
+
include Dry::Equalizer(:type, inspect: false)
|
11
|
+
|
10
12
|
private :options, :meta
|
11
13
|
|
12
14
|
# @param [Object] input
|
@@ -36,7 +38,7 @@ module Dry
|
|
36
38
|
|
37
39
|
# @api public
|
38
40
|
#
|
39
|
-
# @see
|
41
|
+
# @see Nominal#to_ast
|
40
42
|
def to_ast(meta: true)
|
41
43
|
[:safe, [type.to_ast(meta: meta), EMPTY_HASH]]
|
42
44
|
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'dry/types/fn_container'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
# The built-in Hash type can be defined in terms of keys and associated types
|
6
|
+
# its values can contain. Such definitions are named {Schema}s and defined
|
7
|
+
# as lists of {Key} types.
|
8
|
+
#
|
9
|
+
# @see Dry::Types::Schema::Key
|
10
|
+
#
|
11
|
+
# {Schema} evaluates default values for keys missing in input hash
|
12
|
+
#
|
13
|
+
# @see Dry::Types::Default#evaluate
|
14
|
+
# @see Dry::Types::Default::Callable#evaluate
|
15
|
+
#
|
16
|
+
# {Schema} implements Enumerable using its keys as collection.
|
17
|
+
class Schema < Hash
|
18
|
+
NO_TRANSFORM = Dry::Types::FnContainer.register { |x| x }
|
19
|
+
SYMBOLIZE_KEY = Dry::Types::FnContainer.register(:to_sym.to_proc)
|
20
|
+
|
21
|
+
include ::Enumerable
|
22
|
+
|
23
|
+
# @return [Array[Dry::Types::Schema::Key]]
|
24
|
+
attr_reader :keys
|
25
|
+
|
26
|
+
# @return [Hash[Symbol, Dry::Types::Schema::Key]]
|
27
|
+
attr_reader :name_key_map
|
28
|
+
|
29
|
+
# @return [#call]
|
30
|
+
attr_reader :transform_key
|
31
|
+
|
32
|
+
# @param [Class] _primitive
|
33
|
+
# @param [Hash] options
|
34
|
+
# @option options [Array[Dry::Types::Schema::Key]] :keys
|
35
|
+
# @option options [String] :key_transform_fn
|
36
|
+
def initialize(_primitive, **options)
|
37
|
+
@keys = options.fetch(:keys)
|
38
|
+
@name_key_map = keys.each_with_object({}) do |key, idx|
|
39
|
+
idx[key.name] = key
|
40
|
+
end
|
41
|
+
|
42
|
+
key_fn = options.fetch(:key_transform_fn, NO_TRANSFORM)
|
43
|
+
|
44
|
+
@transform_key = Dry::Types::FnContainer[key_fn]
|
45
|
+
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param [Hash] hash
|
50
|
+
# @return [Hash{Symbol => Object}]
|
51
|
+
def call(hash)
|
52
|
+
coerce(hash)
|
53
|
+
end
|
54
|
+
alias_method :[], :call
|
55
|
+
|
56
|
+
# @param [Hash] hash
|
57
|
+
# @option options [Boolean] :skip_missing If true don't raise error if on missing keys
|
58
|
+
# @option options [Boolean] :resolve_defaults If false default value
|
59
|
+
# won't be evaluated for missing key
|
60
|
+
# @return [Hash{Symbol => Object}]
|
61
|
+
def apply(hash, options = EMPTY_HASH)
|
62
|
+
coerce(hash, options)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param [Hash] hash
|
66
|
+
# @yieldparam [Failure] failure
|
67
|
+
# @yieldreturn [Result]
|
68
|
+
# @return [Logic::Result]
|
69
|
+
# @return [Object] if coercion fails and a block is given
|
70
|
+
def try(hash)
|
71
|
+
if hash.is_a?(::Hash)
|
72
|
+
success = true
|
73
|
+
output = {}
|
74
|
+
|
75
|
+
begin
|
76
|
+
result = try_coerce(hash) do |key, key_result|
|
77
|
+
success &&= key_result.success?
|
78
|
+
output[key.name] = key_result.input
|
79
|
+
|
80
|
+
key_result
|
81
|
+
end
|
82
|
+
rescue ConstraintError, UnknownKeysError, SchemaError, MissingKeyError => e
|
83
|
+
success = false
|
84
|
+
result = e
|
85
|
+
end
|
86
|
+
else
|
87
|
+
success = false
|
88
|
+
output = hash
|
89
|
+
result = "#{hash} must be a hash"
|
90
|
+
end
|
91
|
+
|
92
|
+
if success
|
93
|
+
success(output)
|
94
|
+
else
|
95
|
+
failure = failure(output, result)
|
96
|
+
block_given? ? yield(failure) : failure
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param meta [Boolean] Whether to dump the meta to the AST
|
101
|
+
# @return [Array] An AST representation
|
102
|
+
def to_ast(meta: true)
|
103
|
+
if RUBY_VERSION >= "2.5"
|
104
|
+
opts = options.slice(:key_transform_fn, :type_transform_fn, :strict)
|
105
|
+
else
|
106
|
+
opts = options.select { |k, _|
|
107
|
+
k == :key_transform_fn || k == :type_transform_fn || k == :strict
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
[
|
112
|
+
:schema,
|
113
|
+
[
|
114
|
+
keys.map { |key| key.to_ast(meta: meta) },
|
115
|
+
opts,
|
116
|
+
meta ? self.meta : EMPTY_HASH
|
117
|
+
]
|
118
|
+
]
|
119
|
+
end
|
120
|
+
|
121
|
+
# @param [Hash] hash
|
122
|
+
# @return [Boolean]
|
123
|
+
def valid?(hash)
|
124
|
+
result = try(hash)
|
125
|
+
result.success?
|
126
|
+
end
|
127
|
+
alias_method :===, :valid?
|
128
|
+
|
129
|
+
# Whether the schema rejects unknown keys
|
130
|
+
# @return [Boolean]
|
131
|
+
def strict?
|
132
|
+
options.fetch(:strict, false)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Make the schema intolerant to unknown keys
|
136
|
+
# @return [Schema]
|
137
|
+
def strict
|
138
|
+
with(strict: true)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Injects a key transformation function
|
142
|
+
# @param [#call,nil] proc
|
143
|
+
# @param [#call,nil] block
|
144
|
+
# @return [Schema]
|
145
|
+
def with_key_transform(proc = nil, &block)
|
146
|
+
fn = proc || block
|
147
|
+
|
148
|
+
if fn.nil?
|
149
|
+
raise ArgumentError, "a block or callable argument is required"
|
150
|
+
end
|
151
|
+
|
152
|
+
handle = Dry::Types::FnContainer.register(fn)
|
153
|
+
with(key_transform_fn: handle)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Whether the schema transforms input keys
|
157
|
+
# @return [Boolean]
|
158
|
+
# @api public
|
159
|
+
def trasform_keys?
|
160
|
+
!options[:key_transform_fn].nil?
|
161
|
+
end
|
162
|
+
|
163
|
+
# @overload schema(type_map, meta = EMPTY_HASH)
|
164
|
+
# @param [{Symbol => Dry::Types::Nominal}] type_map
|
165
|
+
# @param [Hash] meta
|
166
|
+
# @return [Dry::Types::Schema]
|
167
|
+
# @overload schema(keys)
|
168
|
+
# @param [Array<Dry::Types::Schema::Key>] key List of schema keys
|
169
|
+
# @param [Hash] meta
|
170
|
+
# @return [Dry::Types::Schema]
|
171
|
+
def schema(keys_or_map)
|
172
|
+
if keys_or_map.is_a?(::Array)
|
173
|
+
new_keys = keys_or_map
|
174
|
+
else
|
175
|
+
new_keys = build_keys(keys_or_map)
|
176
|
+
end
|
177
|
+
|
178
|
+
keys = merge_keys(self.keys, new_keys)
|
179
|
+
Schema.new(primitive, **options, keys: keys, meta: meta)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Iterate over each key type
|
183
|
+
#
|
184
|
+
# @return [Array<Dry::Types::Schema::Key>,Enumerator]
|
185
|
+
def each(&block)
|
186
|
+
keys.each(&block)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Whether the schema has the given key
|
190
|
+
#
|
191
|
+
# @param [Symbol] name Key name
|
192
|
+
# @return [Boolean]
|
193
|
+
def key?(name)
|
194
|
+
name_key_map.key?(name)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Fetch key type by a key name.
|
198
|
+
# Behaves as ::Hash#fetch
|
199
|
+
#
|
200
|
+
# @overload key(name, fallback = Undefined)
|
201
|
+
# @param [Symbol] name Key name
|
202
|
+
# @param [Object] fallback Optional fallback, returned if key is missing
|
203
|
+
# @return [Dry::Types::Schema::Key,Object] key type or fallback if key is not in schema
|
204
|
+
#
|
205
|
+
# @overload key(name, &block)
|
206
|
+
# @param [Symbol] name Key name
|
207
|
+
# @param [Proc] block Fallback block, runs if key is missing
|
208
|
+
# @return [Dry::Types::Schema::Key,Object] key type or block value if key is not in schema
|
209
|
+
def key(name, fallback = Undefined, &block)
|
210
|
+
if Undefined.equal?(fallback)
|
211
|
+
name_key_map.fetch(name, &block)
|
212
|
+
else
|
213
|
+
name_key_map.fetch(name, fallback)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# @return [Boolean]
|
218
|
+
def constrained?
|
219
|
+
true
|
220
|
+
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# @param [Array<Dry::Types::Schema::Keys>] keys
|
225
|
+
# @return [Dry::Types::Schema]
|
226
|
+
# @api private
|
227
|
+
def merge_keys(*keys)
|
228
|
+
keys.
|
229
|
+
flatten(1).
|
230
|
+
each_with_object({}) { |key, merged| merged[key.name] = key }.
|
231
|
+
values
|
232
|
+
end
|
233
|
+
|
234
|
+
def resolve(hash, options = EMPTY_HASH, &block)
|
235
|
+
result = {}
|
236
|
+
|
237
|
+
hash.each do |key, value|
|
238
|
+
k = transform_key.(key)
|
239
|
+
|
240
|
+
if name_key_map.key?(k)
|
241
|
+
result[k] = yield(name_key_map[k], value)
|
242
|
+
elsif strict?
|
243
|
+
raise UnknownKeysError.new(*unexpected_keys(hash.keys))
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
if result.size < keys.size
|
248
|
+
resolve_missing_keys(result, options, &block)
|
249
|
+
end
|
250
|
+
|
251
|
+
result
|
252
|
+
end
|
253
|
+
|
254
|
+
def resolve_missing_keys(result, options)
|
255
|
+
skip_missing = options.fetch(:skip_missing, false)
|
256
|
+
resolve_defaults = options.fetch(:resolve_defaults, true)
|
257
|
+
|
258
|
+
keys.each do |key|
|
259
|
+
next if result.key?(key.name)
|
260
|
+
|
261
|
+
if key.default? && resolve_defaults
|
262
|
+
result[key.name] = yield(key, Undefined)
|
263
|
+
elsif key.required? && !skip_missing
|
264
|
+
raise MissingKeyError, key.name
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# @param keys [Array<Symbol>]
|
270
|
+
# @return [Array<Symbol>]
|
271
|
+
def unexpected_keys(keys)
|
272
|
+
keys.map(&transform_key) - name_key_map.keys
|
273
|
+
end
|
274
|
+
|
275
|
+
# @param [Hash] hash
|
276
|
+
# @return [Hash{Symbol => Object}]
|
277
|
+
def try_coerce(hash)
|
278
|
+
resolve(hash) do |key, value|
|
279
|
+
yield(key, key.try(value))
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# @param [Hash] hash
|
284
|
+
# @return [Hash{Symbol => Object}]
|
285
|
+
def coerce(hash, options = EMPTY_HASH)
|
286
|
+
resolve(hash, options) do |key, value|
|
287
|
+
begin
|
288
|
+
key.(value)
|
289
|
+
rescue ConstraintError => e
|
290
|
+
raise SchemaError.new(key.name, value, e.result)
|
291
|
+
rescue TypeError, ArgumentError => e
|
292
|
+
raise SchemaError.new(key.name, value, e.message)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Types
|
5
|
+
class Schema < Hash
|
6
|
+
# Proxy type for schema keys. Contains only key name and
|
7
|
+
# whether it's required or not. All other calls deletaged
|
8
|
+
# to the wrapped type.
|
9
|
+
#
|
10
|
+
# @see Dry::Types::Schema
|
11
|
+
class Key
|
12
|
+
include Type
|
13
|
+
include Dry::Equalizer(:name, :type, :options, inspect: false)
|
14
|
+
include Decorator
|
15
|
+
include Builder
|
16
|
+
include Printable
|
17
|
+
|
18
|
+
# @return [Symbol]
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# @api private
|
22
|
+
def initialize(type, name, required: Undefined, **options)
|
23
|
+
required = Undefined.default(required) do
|
24
|
+
type.meta.fetch(:required) { !type.meta.fetch(:omittable, false) }
|
25
|
+
end
|
26
|
+
|
27
|
+
super(type, name, required: required, **options)
|
28
|
+
@name = name
|
29
|
+
end
|
30
|
+
|
31
|
+
# @see Dry::Types::Nominal#call
|
32
|
+
def call(input, &block)
|
33
|
+
type.(input, &block)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @see Dry::Types::Nominal#try
|
37
|
+
def try(input, &block)
|
38
|
+
type.try(input, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Whether the key is required in schema input
|
42
|
+
#
|
43
|
+
# @return [Boolean]
|
44
|
+
def required?
|
45
|
+
options.fetch(:required)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Control whether the key is required
|
49
|
+
#
|
50
|
+
# @overload required
|
51
|
+
# @return [Boolean]
|
52
|
+
#
|
53
|
+
# @overload required(required)
|
54
|
+
# Change key's "requireness"
|
55
|
+
#
|
56
|
+
# @param [Boolean] required New value
|
57
|
+
# @return [Dry::Types::Schema::Key]
|
58
|
+
def required(required = Undefined)
|
59
|
+
if Undefined.equal?(required)
|
60
|
+
options.fetch(:required)
|
61
|
+
else
|
62
|
+
with(required: required)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Make key not required
|
67
|
+
#
|
68
|
+
# @return [Dry::Types::Schema::Key]
|
69
|
+
def omittable
|
70
|
+
required(false)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Construct a default type. Default values are
|
74
|
+
# evaluated/applied when key is absent in schema
|
75
|
+
# input.
|
76
|
+
#
|
77
|
+
# @see Dry::Types::Default
|
78
|
+
# @return [Dry::Types::Schema::Key]
|
79
|
+
def default(input = Undefined, &block)
|
80
|
+
new(type.default(input, &block))
|
81
|
+
end
|
82
|
+
|
83
|
+
# Replace the underlying type
|
84
|
+
# @param [Dry::Types::Type] type
|
85
|
+
# @return [Dry::Types::Schema::Key]
|
86
|
+
def new(type)
|
87
|
+
self.class.new(type, name, options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @see Dry::Types::Safe
|
91
|
+
# @return [Dry::Types::Schema::Key]
|
92
|
+
def safe
|
93
|
+
new(type.safe)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Dump to internal AST representation
|
97
|
+
#
|
98
|
+
# @return [Array]
|
99
|
+
def to_ast(meta: true)
|
100
|
+
[
|
101
|
+
:key,
|
102
|
+
[
|
103
|
+
name,
|
104
|
+
required,
|
105
|
+
type.to_ast(meta: meta)
|
106
|
+
]
|
107
|
+
]
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get/set type metadata. The Key type doesn't have
|
111
|
+
# its out meta, it delegates these calls to the underlying
|
112
|
+
# type.
|
113
|
+
#
|
114
|
+
# @overload meta
|
115
|
+
# @return [Hash] metadata associated with type
|
116
|
+
#
|
117
|
+
# @overload meta(data)
|
118
|
+
# @param [Hash] new metadata to merge into existing metadata
|
119
|
+
# @return [Type] new type with added metadata
|
120
|
+
def meta(data = nil)
|
121
|
+
if data.nil?
|
122
|
+
type.meta
|
123
|
+
else
|
124
|
+
new(type.meta(data))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|