dry-transformer 0.1.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 +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
|