dry-transformer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +66 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +16 -0
- data/.rspec +4 -0
- data/.rubocop.yml +95 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +19 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/docsite/source/built-in-transformations.html.md +47 -0
- data/docsite/source/index.html.md +15 -0
- data/docsite/source/transformation-objects.html.md +32 -0
- data/docsite/source/using-standalone-functions.html.md +82 -0
- data/dry-transformer.gemspec +22 -0
- data/lib/dry-transformer.rb +3 -0
- data/lib/dry/transformer.rb +23 -0
- data/lib/dry/transformer/all.rb +11 -0
- data/lib/dry/transformer/array.rb +183 -0
- data/lib/dry/transformer/array/combine.rb +65 -0
- data/lib/dry/transformer/class.rb +56 -0
- data/lib/dry/transformer/coercions.rb +196 -0
- data/lib/dry/transformer/compiler.rb +47 -0
- data/lib/dry/transformer/composite.rb +54 -0
- data/lib/dry/transformer/conditional.rb +76 -0
- data/lib/dry/transformer/constants.rb +7 -0
- data/lib/dry/transformer/error.rb +16 -0
- data/lib/dry/transformer/function.rb +109 -0
- data/lib/dry/transformer/hash.rb +453 -0
- data/lib/dry/transformer/pipe.rb +75 -0
- data/lib/dry/transformer/pipe/class_interface.rb +115 -0
- data/lib/dry/transformer/pipe/dsl.rb +58 -0
- data/lib/dry/transformer/proc.rb +46 -0
- data/lib/dry/transformer/recursion.rb +121 -0
- data/lib/dry/transformer/registry.rb +150 -0
- data/lib/dry/transformer/store.rb +128 -0
- data/lib/dry/transformer/version.rb +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/array/combine_spec.rb +224 -0
- data/spec/unit/array_transformations_spec.rb +233 -0
- data/spec/unit/class_transformations_spec.rb +50 -0
- data/spec/unit/coercions_spec.rb +132 -0
- data/spec/unit/conditional_spec.rb +48 -0
- data/spec/unit/function_not_found_error_spec.rb +12 -0
- data/spec/unit/function_spec.rb +193 -0
- data/spec/unit/hash_transformations_spec.rb +490 -0
- data/spec/unit/proc_transformations_spec.rb +20 -0
- data/spec/unit/recursion_spec.rb +145 -0
- data/spec/unit/registry_spec.rb +202 -0
- data/spec/unit/store_spec.rb +198 -0
- data/spec/unit/transformer/class_interface_spec.rb +350 -0
- data/spec/unit/transformer/dsl_spec.rb +15 -0
- data/spec/unit/transformer/instance_methods_spec.rb +25 -0
- metadata +119 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/transformer/composite'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Transformer
|
7
|
+
# Transformation proc wrapper allowing composition of multiple procs into
|
8
|
+
# a data-transformation pipeline.
|
9
|
+
#
|
10
|
+
# This is used by Dry::Transformer to wrap registered methods.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class Function
|
14
|
+
# Wrapped proc or another composite function
|
15
|
+
#
|
16
|
+
# @return [Proc,Composed]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
attr_reader :fn
|
20
|
+
|
21
|
+
# Additional arguments that will be passed to the wrapped proc
|
22
|
+
#
|
23
|
+
# @return [Array]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
attr_reader :args
|
27
|
+
|
28
|
+
# @!attribute [r] name
|
29
|
+
#
|
30
|
+
# @return [<type] The name of the function
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
attr_reader :name
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def initialize(fn, options = {})
|
37
|
+
@fn = fn
|
38
|
+
@args = options.fetch(:args, [])
|
39
|
+
@name = options.fetch(:name, fn)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call the wrapped proc
|
43
|
+
#
|
44
|
+
# @param [Object] value The input value
|
45
|
+
#
|
46
|
+
# @alias []
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def call(*value)
|
50
|
+
fn.call(*value, *args)
|
51
|
+
end
|
52
|
+
alias_method :[], :call
|
53
|
+
|
54
|
+
# Compose this function with another function or a proc
|
55
|
+
#
|
56
|
+
# @param [Proc,Function]
|
57
|
+
#
|
58
|
+
# @return [Composite]
|
59
|
+
#
|
60
|
+
# @alias :>>
|
61
|
+
#
|
62
|
+
# @api public
|
63
|
+
def compose(other)
|
64
|
+
Composite.new(self, other)
|
65
|
+
end
|
66
|
+
alias_method :+, :compose
|
67
|
+
alias_method :>>, :compose
|
68
|
+
|
69
|
+
# Return a new fn with curried args
|
70
|
+
#
|
71
|
+
# @return [Function]
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
def with(*args)
|
75
|
+
self.class.new(fn, name: name, args: args)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api public
|
79
|
+
def ==(other)
|
80
|
+
return false unless other.instance_of?(self.class)
|
81
|
+
|
82
|
+
[fn, name, args] == [other.fn, other.name, other.args]
|
83
|
+
end
|
84
|
+
alias_method :eql?, :==
|
85
|
+
|
86
|
+
# Return a simple AST representation of this function
|
87
|
+
#
|
88
|
+
# @return [Array]
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def to_ast
|
92
|
+
args_ast = args.map { |arg| arg.respond_to?(:to_ast) ? arg.to_ast : arg }
|
93
|
+
[name, args_ast]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Converts a transproc to a simple proc
|
97
|
+
#
|
98
|
+
# @return [Proc]
|
99
|
+
#
|
100
|
+
def to_proc
|
101
|
+
if !args.empty?
|
102
|
+
proc { |*value| fn.call(*value, *args) }
|
103
|
+
else
|
104
|
+
fn.to_proc
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,453 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/transformer/coercions'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Transformer
|
7
|
+
# Transformation functions for Hash objects
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# require 'dry/transformer/hash'
|
11
|
+
#
|
12
|
+
# include Dry::Transformer::Helper
|
13
|
+
#
|
14
|
+
# fn = t(:symbolize_keys) >> t(:nest, :address, [:street, :zipcode])
|
15
|
+
#
|
16
|
+
# fn["street" => "Street 1", "zipcode" => "123"]
|
17
|
+
# # => {:address => {:street => "Street 1", :zipcode => "123"}}
|
18
|
+
#
|
19
|
+
# @api public
|
20
|
+
module HashTransformations
|
21
|
+
extend Registry
|
22
|
+
|
23
|
+
if RUBY_VERSION >= '2.5'
|
24
|
+
# Map all keys in a hash with the provided transformation function
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
# Dry::Transformer(:map_keys, -> s { s.upcase })['name' => 'Jane']
|
28
|
+
# # => {"NAME" => "Jane"}
|
29
|
+
#
|
30
|
+
# @param [Hash]
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def self.map_keys(source_hash, fn)
|
36
|
+
Hash[source_hash].transform_keys!(&fn)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
def self.map_keys(source_hash, fn)
|
40
|
+
Hash[source_hash].tap do |hash|
|
41
|
+
hash.keys.each { |key| hash[fn[key]] = hash.delete(key) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Symbolize all keys in a hash
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# Dry::Transformer(:symbolize_keys)['name' => 'Jane']
|
50
|
+
# # => {:name => "Jane"}
|
51
|
+
#
|
52
|
+
# @param [Hash]
|
53
|
+
#
|
54
|
+
# @return [Hash]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def self.symbolize_keys(hash)
|
58
|
+
map_keys(hash, Coercions[:to_symbol].fn)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Symbolize keys in a hash recursively
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
#
|
65
|
+
# input = { 'foo' => 'bar', 'baz' => [{ 'one' => 1 }] }
|
66
|
+
#
|
67
|
+
# t(:deep_symbolize_keys)[input]
|
68
|
+
# # => { :foo => "bar", :baz => [{ :one => 1 }] }
|
69
|
+
#
|
70
|
+
# @param [Hash]
|
71
|
+
#
|
72
|
+
# @return [Hash]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
def self.deep_symbolize_keys(hash)
|
76
|
+
hash.each_with_object({}) do |(key, value), output|
|
77
|
+
output[key.to_sym] =
|
78
|
+
case value
|
79
|
+
when Hash
|
80
|
+
deep_symbolize_keys(value)
|
81
|
+
when Array
|
82
|
+
value.map { |item|
|
83
|
+
item.is_a?(Hash) ? deep_symbolize_keys(item) : item
|
84
|
+
}
|
85
|
+
else
|
86
|
+
value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Stringify all keys in a hash
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# Dry::Transformer(:stringify_keys)[:name => 'Jane']
|
95
|
+
# # => {"name" => "Jane"}
|
96
|
+
#
|
97
|
+
# @param [Hash]
|
98
|
+
#
|
99
|
+
# @return [Hash]
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
def self.stringify_keys(hash)
|
103
|
+
map_keys(hash, Coercions[:to_string].fn)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Stringify keys in a hash recursively
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# input = { :foo => "bar", :baz => [{ :one => 1 }] }
|
110
|
+
#
|
111
|
+
# t(:deep_stringify_keys)[input]
|
112
|
+
# # => { "foo" => "bar", "baz" => [{ "one" => 1 }] }
|
113
|
+
#
|
114
|
+
# @param [Hash]
|
115
|
+
#
|
116
|
+
# @return [Hash]
|
117
|
+
#
|
118
|
+
# @api public
|
119
|
+
def self.deep_stringify_keys(hash)
|
120
|
+
hash.each_with_object({}) do |(key, value), output|
|
121
|
+
output[key.to_s] =
|
122
|
+
case value
|
123
|
+
when Hash
|
124
|
+
deep_stringify_keys(value)
|
125
|
+
when Array
|
126
|
+
value.map { |item|
|
127
|
+
item.is_a?(Hash) ? deep_stringify_keys(item) : item
|
128
|
+
}
|
129
|
+
else
|
130
|
+
value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
if RUBY_VERSION >= '2.4'
|
136
|
+
# Map all values in a hash using transformation function
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# Dry::Transformer(:map_values, -> v { v.upcase })[:name => 'Jane']
|
140
|
+
# # => {"name" => "JANE"}
|
141
|
+
#
|
142
|
+
# @param [Hash]
|
143
|
+
#
|
144
|
+
# @return [Hash]
|
145
|
+
#
|
146
|
+
# @api public
|
147
|
+
def self.map_values(source_hash, fn)
|
148
|
+
Hash[source_hash].transform_values!(&fn)
|
149
|
+
end
|
150
|
+
else
|
151
|
+
def self.map_values(source_hash, fn)
|
152
|
+
Hash[source_hash].tap do |hash|
|
153
|
+
hash.each { |key, value| hash[key] = fn[value] }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Rename all keys in a hash using provided mapping hash
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# Dry::Transformer(:rename_keys, user_name: :name)[user_name: 'Jane']
|
162
|
+
# # => {:name => "Jane"}
|
163
|
+
#
|
164
|
+
# @param [Hash] source_hash The input hash
|
165
|
+
# @param [Hash] mapping The key-rename mapping
|
166
|
+
#
|
167
|
+
# @return [Hash]
|
168
|
+
#
|
169
|
+
# @api public
|
170
|
+
def self.rename_keys(source_hash, mapping)
|
171
|
+
Hash[source_hash].tap do |hash|
|
172
|
+
mapping.each { |k, v| hash[v] = hash.delete(k) if hash.key?(k) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Copy all keys in a hash using provided mapping hash
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# Dry::Transformer(:copy_keys, user_name: :name)[user_name: 'Jane']
|
180
|
+
# # => {:user_name => "Jane", :name => "Jane"}
|
181
|
+
#
|
182
|
+
# @param [Hash] source_hash The input hash
|
183
|
+
# @param [Hash] mapping The key-copy mapping
|
184
|
+
#
|
185
|
+
# @return [Hash]
|
186
|
+
#
|
187
|
+
# @api public
|
188
|
+
def self.copy_keys(source_hash, mapping)
|
189
|
+
Hash[source_hash].tap do |hash|
|
190
|
+
mapping.each do |original_key, new_keys|
|
191
|
+
[*new_keys].each do |new_key|
|
192
|
+
hash[new_key] = hash[original_key]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Rejects specified keys from a hash
|
199
|
+
#
|
200
|
+
# @example
|
201
|
+
# Dry::Transformer(:reject_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
|
202
|
+
# # => {:email => "jane@doe.org"}
|
203
|
+
#
|
204
|
+
# @param [Hash] hash The input hash
|
205
|
+
# @param [Array] keys The keys to be rejected
|
206
|
+
#
|
207
|
+
# @return [Hash]
|
208
|
+
#
|
209
|
+
# @api public
|
210
|
+
def self.reject_keys(hash, keys)
|
211
|
+
Hash[hash].reject { |k, _| keys.include?(k) }
|
212
|
+
end
|
213
|
+
|
214
|
+
if RUBY_VERSION >= '2.5'
|
215
|
+
# Accepts specified keys from a hash
|
216
|
+
#
|
217
|
+
# @example
|
218
|
+
# Dry::Transformer(:accept_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
|
219
|
+
# # => {:name=>"Jane"}
|
220
|
+
#
|
221
|
+
# @param [Hash] hash The input hash
|
222
|
+
# @param [Array] keys The keys to be accepted
|
223
|
+
#
|
224
|
+
# @return [Hash]
|
225
|
+
#
|
226
|
+
# @api public
|
227
|
+
def self.accept_keys(hash, keys)
|
228
|
+
Hash[hash].slice(*keys)
|
229
|
+
end
|
230
|
+
else
|
231
|
+
def self.accept_keys(hash, keys)
|
232
|
+
reject_keys(hash, hash.keys - keys)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Map a key in a hash with the provided transformation function
|
237
|
+
#
|
238
|
+
# @example
|
239
|
+
# Dry::Transformer(:map_value, 'name', -> s { s.upcase })['name' => 'jane']
|
240
|
+
# # => {"name" => "JANE"}
|
241
|
+
#
|
242
|
+
# @param [Hash]
|
243
|
+
#
|
244
|
+
# @return [Hash]
|
245
|
+
#
|
246
|
+
# @api public
|
247
|
+
def self.map_value(hash, key, fn)
|
248
|
+
hash.merge(key => fn[hash[key]])
|
249
|
+
end
|
250
|
+
|
251
|
+
# Nest values from specified keys under a new key
|
252
|
+
#
|
253
|
+
# @example
|
254
|
+
# Dry::Transformer(:nest, :address, [:street, :zipcode])[street: 'Street', zipcode: '123']
|
255
|
+
# # => {address: {street: "Street", zipcode: "123"}}
|
256
|
+
#
|
257
|
+
# @param [Hash]
|
258
|
+
#
|
259
|
+
# @return [Hash]
|
260
|
+
#
|
261
|
+
# @api public
|
262
|
+
def self.nest(hash, root, keys)
|
263
|
+
child = {}
|
264
|
+
|
265
|
+
keys.each do |key|
|
266
|
+
child[key] = hash[key] if hash.key?(key)
|
267
|
+
end
|
268
|
+
|
269
|
+
output = Hash[hash]
|
270
|
+
|
271
|
+
child.each_key { |key| output.delete(key) }
|
272
|
+
|
273
|
+
old_root = hash[root]
|
274
|
+
|
275
|
+
if old_root.is_a?(Hash)
|
276
|
+
output[root] = old_root.merge(child)
|
277
|
+
else
|
278
|
+
output[root] = child
|
279
|
+
end
|
280
|
+
|
281
|
+
output
|
282
|
+
end
|
283
|
+
|
284
|
+
# Collapse a nested hash from a specified key
|
285
|
+
#
|
286
|
+
# @example
|
287
|
+
# Dry::Transformer(:unwrap, :address, [:street, :zipcode])[address: { street: 'Street', zipcode: '123' }]
|
288
|
+
# # => {street: "Street", zipcode: "123"}
|
289
|
+
#
|
290
|
+
# @param [Hash] source_hash
|
291
|
+
# @param [Mixed] root The root key to unwrap values from
|
292
|
+
# @param [Array] selected The keys that should be unwrapped (optional)
|
293
|
+
# @param [Hash] options hash of options (optional)
|
294
|
+
# @option options [Boolean] :prefix if true, unwrapped keys will be prefixed
|
295
|
+
# with the root key followed by an underscore (_)
|
296
|
+
#
|
297
|
+
# @return [Hash]
|
298
|
+
#
|
299
|
+
# @api public
|
300
|
+
def self.unwrap(source_hash, root, selected = nil, prefix: false)
|
301
|
+
return source_hash unless source_hash[root]
|
302
|
+
|
303
|
+
add_prefix = lambda do |key|
|
304
|
+
combined = [root, key].join('_')
|
305
|
+
root.is_a?(::Symbol) ? combined.to_sym : combined
|
306
|
+
end
|
307
|
+
|
308
|
+
Hash[source_hash].merge(root => Hash[source_hash[root]]).tap do |hash|
|
309
|
+
nested_hash = hash[root]
|
310
|
+
keys = nested_hash.keys
|
311
|
+
keys &= selected if selected
|
312
|
+
new_keys = prefix ? keys.map(&add_prefix) : keys
|
313
|
+
|
314
|
+
hash.update(Hash[new_keys.zip(keys.map { |key| nested_hash.delete(key) })])
|
315
|
+
hash.delete(root) if nested_hash.empty?
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Folds array of tuples to array of values from a specified key
|
320
|
+
#
|
321
|
+
# @example
|
322
|
+
# source = {
|
323
|
+
# name: "Jane",
|
324
|
+
# tasks: [{ title: "be nice", priority: 1 }, { title: "sleep well" }]
|
325
|
+
# }
|
326
|
+
# Dry::Transformer(:fold, :tasks, :title)[source]
|
327
|
+
# # => { name: "Jane", tasks: ["be nice", "sleep well"] }
|
328
|
+
# Dry::Transformer(:fold, :tasks, :priority)[source]
|
329
|
+
# # => { name: "Jane", tasks: [1, nil] }
|
330
|
+
#
|
331
|
+
# @param [Hash] hash
|
332
|
+
# @param [Object] key The key to fold values to
|
333
|
+
# @param [Object] tuple_key The key to take folded values from
|
334
|
+
#
|
335
|
+
# @return [Hash]
|
336
|
+
#
|
337
|
+
# @api public
|
338
|
+
def self.fold(hash, key, tuple_key)
|
339
|
+
hash.merge(key => ArrayTransformations.extract_key(hash[key], tuple_key))
|
340
|
+
end
|
341
|
+
|
342
|
+
# Splits hash to array by all values from a specified key
|
343
|
+
#
|
344
|
+
# The operation adds missing keys extracted from the array to regularize the output.
|
345
|
+
#
|
346
|
+
# @example
|
347
|
+
# input = {
|
348
|
+
# name: 'Joe',
|
349
|
+
# tasks: [
|
350
|
+
# { title: 'sleep well', priority: 1 },
|
351
|
+
# { title: 'be nice', priority: 2 },
|
352
|
+
# { priority: 2 },
|
353
|
+
# { title: 'be cool' }
|
354
|
+
# ]
|
355
|
+
# }
|
356
|
+
# Dry::Transformer(:split, :tasks, [:priority])[input]
|
357
|
+
# => [
|
358
|
+
# { name: 'Joe', priority: 1, tasks: [{ title: 'sleep well' }] },
|
359
|
+
# { name: 'Joe', priority: 2, tasks: [{ title: 'be nice' }, { title: nil }] },
|
360
|
+
# { name: 'Joe', priority: nil, tasks: [{ title: 'be cool' }] }
|
361
|
+
# ]
|
362
|
+
#
|
363
|
+
# @param [Hash] hash
|
364
|
+
# @param [Object] key The key to split a hash by
|
365
|
+
# @param [Array] subkeys The list of subkeys to be extracted from key
|
366
|
+
#
|
367
|
+
# @return [Array<Hash>]
|
368
|
+
#
|
369
|
+
# @api public
|
370
|
+
def self.split(hash, key, keys)
|
371
|
+
list = Array(hash[key])
|
372
|
+
return [hash.reject { |k, _| k == key }] if list.empty?
|
373
|
+
|
374
|
+
existing = list.flat_map(&:keys).uniq
|
375
|
+
grouped = existing - keys
|
376
|
+
ungrouped = existing & keys
|
377
|
+
|
378
|
+
list = ArrayTransformations.group(list, key, grouped) if grouped.any?
|
379
|
+
list = list.map { |item| item.merge(reject_keys(hash, [key])) }
|
380
|
+
ArrayTransformations.add_keys(list, ungrouped)
|
381
|
+
end
|
382
|
+
|
383
|
+
# Recursively evaluate hash values if they are procs/lambdas
|
384
|
+
#
|
385
|
+
# @example
|
386
|
+
# hash = {
|
387
|
+
# num: -> i { i + 1 },
|
388
|
+
# str: -> i { "num #{i}" }
|
389
|
+
# }
|
390
|
+
#
|
391
|
+
# t(:eval_values, 1)[hash]
|
392
|
+
# # => {:num => 2, :str => "num 1" }
|
393
|
+
#
|
394
|
+
# # with filters
|
395
|
+
# t(:eval_values, 1, [:str])[hash]
|
396
|
+
# # => {:num => #{still a proc}, :str => "num 1" }
|
397
|
+
#
|
398
|
+
# @param [Hash]
|
399
|
+
# @param [Array,Object] args Anything that should be passed to procs
|
400
|
+
# @param [Array] filters A list of attribute names that should be evaluated
|
401
|
+
#
|
402
|
+
# @api public
|
403
|
+
def self.eval_values(hash, args, filters = [])
|
404
|
+
hash.each_with_object({}) do |(key, value), output|
|
405
|
+
output[key] =
|
406
|
+
case value
|
407
|
+
when Proc
|
408
|
+
if filters.empty? || filters.include?(key)
|
409
|
+
value.call(*args)
|
410
|
+
else
|
411
|
+
value
|
412
|
+
end
|
413
|
+
when Hash
|
414
|
+
eval_values(value, args, filters)
|
415
|
+
when Array
|
416
|
+
value.map { |item|
|
417
|
+
item.is_a?(Hash) ? eval_values(item, args, filters) : item
|
418
|
+
}
|
419
|
+
else
|
420
|
+
value
|
421
|
+
end
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# Merge a hash recursively
|
426
|
+
#
|
427
|
+
# @example
|
428
|
+
#
|
429
|
+
# input = { 'foo' => 'bar', 'baz' => { 'one' => 1 } }
|
430
|
+
# other = { 'foo' => 'buz', 'baz' => { :one => 'one', :two => 2 } }
|
431
|
+
#
|
432
|
+
# t(:deep_merge)[input, other]
|
433
|
+
# # => { 'foo' => "buz", :baz => { :one => 'one', 'one' => 1, :two => 2 } }
|
434
|
+
#
|
435
|
+
# @param [Hash]
|
436
|
+
# @param [Hash]
|
437
|
+
#
|
438
|
+
# @return [Hash]
|
439
|
+
#
|
440
|
+
# @api public
|
441
|
+
def self.deep_merge(hash, other)
|
442
|
+
Hash[hash].merge(other) do |_, original_value, new_value|
|
443
|
+
if original_value.respond_to?(:to_hash) &&
|
444
|
+
new_value.respond_to?(:to_hash)
|
445
|
+
deep_merge(Hash[original_value], Hash[new_value])
|
446
|
+
else
|
447
|
+
new_value
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
end
|