hashie 4.1.0 → 5.2.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/CHANGELOG.md +138 -53
- data/CONTRIBUTING.md +38 -1
- data/LICENSE +1 -1
- data/README.md +920 -205
- data/Rakefile +32 -1
- data/UPGRADING.md +56 -0
- data/hashie.gemspec +10 -5
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +160 -32
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -26
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
- data/lib/hashie/extensions/dash/property_translation.rb +138 -15
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +16 -29
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
- data/lib/hashie/extensions/indifferent_access.rb +124 -5
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +94 -8
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +84 -17
- data/lib/hashie/hash.rb +18 -2
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +314 -82
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +46 -0
- metadata +22 -9
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Dash
|
|
@@ -7,12 +9,12 @@ module Hashie
|
|
|
7
9
|
# application -- such as a Java API -- where the keys are named
|
|
8
10
|
# differently from Ruby conventions.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
12
|
+
# @example Translate inconsistent API keys
|
|
11
13
|
#
|
|
12
14
|
# class PersonHash < Hashie::Dash
|
|
13
15
|
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
14
16
|
#
|
|
15
|
-
# property :first_name, from :firstName
|
|
17
|
+
# property :first_name, from: :firstName
|
|
16
18
|
# property :last_name, from: :lastName
|
|
17
19
|
# property :first_name, from: :f_name
|
|
18
20
|
# property :last_name, from: :l_name
|
|
@@ -25,7 +27,8 @@ module Hashie
|
|
|
25
27
|
# You can also use a lambda to translate the value. This is particularly
|
|
26
28
|
# useful when you want to ensure the type of data you're wrapping.
|
|
27
29
|
#
|
|
28
|
-
#
|
|
30
|
+
# @example Transform input values
|
|
31
|
+
# require 'time'
|
|
29
32
|
#
|
|
30
33
|
# class DataModelHash < Hashie::Dash
|
|
31
34
|
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
@@ -35,28 +38,66 @@ module Hashie
|
|
|
35
38
|
# end
|
|
36
39
|
#
|
|
37
40
|
# model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
|
|
38
|
-
# model.id.class #=> Integer
|
|
41
|
+
# model.id.class #=> Integer
|
|
39
42
|
# model.created_at.class #=> Time
|
|
40
43
|
module PropertyTranslation
|
|
44
|
+
# @example Install translation declarations on a Dash
|
|
45
|
+
# klass = Class.new(Hashie::Dash) do
|
|
46
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
47
|
+
# property :name, from: :fullName
|
|
48
|
+
# end
|
|
49
|
+
# klass.new(fullName: 'Ada').name # => 'Ada'
|
|
41
50
|
def self.included(base)
|
|
42
51
|
base.instance_variable_set(:@transforms, {})
|
|
43
52
|
base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
|
|
44
53
|
base.extend(ClassMethods)
|
|
45
54
|
base.send(:include, InstanceMethods)
|
|
55
|
+
base.extend Hashie::Extensions::DeepMerge
|
|
46
56
|
end
|
|
47
57
|
|
|
58
|
+
# Defines input translations and value transformations.
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# klass = Class.new(Hashie::Dash) do
|
|
62
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
63
|
+
# property :name, from: :fullName
|
|
64
|
+
# end
|
|
65
|
+
# klass.translations # => {fullName: :name}
|
|
48
66
|
module ClassMethods
|
|
67
|
+
# @example Inspect value transformers
|
|
68
|
+
# klass = Class.new(Hashie::Dash) do
|
|
69
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
70
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
71
|
+
# end
|
|
72
|
+
# klass.transforms[:age].call('37') # => 37
|
|
73
|
+
# klass.translations_hash # => {}
|
|
49
74
|
attr_reader :transforms, :translations_hash
|
|
50
75
|
|
|
51
76
|
# Ensures that any inheriting classes maintain their translations.
|
|
52
77
|
#
|
|
53
|
-
# *
|
|
78
|
+
# * `:default` - The class inheriting the translations.
|
|
79
|
+
#
|
|
80
|
+
# @example Copy translation settings to subclasses
|
|
81
|
+
# parent = Class.new(Hashie::Dash) do
|
|
82
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
83
|
+
# property :name, from: :fullName
|
|
84
|
+
# end
|
|
85
|
+
# child = Class.new(parent) { property :age, from: :years }
|
|
86
|
+
# child.new(fullName: 'Ada', years: 37).age # => 37
|
|
87
|
+
# parent.translations # => {fullName: :name}
|
|
54
88
|
def inherited(klass)
|
|
55
89
|
super
|
|
56
90
|
klass.instance_variable_set(:@transforms, transforms.dup)
|
|
57
|
-
klass.instance_variable_set(:@translations_hash, translations_hash
|
|
91
|
+
klass.instance_variable_set(:@translations_hash, _deep_dup(translations_hash))
|
|
58
92
|
end
|
|
59
93
|
|
|
94
|
+
# @example List translated input keys alongside untranslated properties
|
|
95
|
+
# klass = Class.new(Hashie::Dash) do
|
|
96
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
97
|
+
# property :name, from: :fullName
|
|
98
|
+
# property :age
|
|
99
|
+
# end
|
|
100
|
+
# klass.permitted_input_keys # => [:fullName, :age]
|
|
60
101
|
def permitted_input_keys
|
|
61
102
|
@permitted_input_keys ||=
|
|
62
103
|
properties
|
|
@@ -65,12 +106,19 @@ module Hashie
|
|
|
65
106
|
|
|
66
107
|
# Defines a property on the Trash. Options are as follows:
|
|
67
108
|
#
|
|
68
|
-
# *
|
|
109
|
+
# * `:default` - Specify a default value for this property, to be
|
|
69
110
|
# returned before a value is set on the property in a new Dash.
|
|
70
|
-
# *
|
|
71
|
-
# *
|
|
72
|
-
# *
|
|
111
|
+
# * `:from` - Specify the original key name that will be write only.
|
|
112
|
+
# * `:with` - Specify a lambda to be used to convert value.
|
|
113
|
+
# * `:transform_with` - Specify a lambda to be used to convert value
|
|
73
114
|
# without using the :from option. It transform the property itself.
|
|
115
|
+
#
|
|
116
|
+
# @example Declare a translated and converted property
|
|
117
|
+
# klass = Class.new(Hashie::Dash) do
|
|
118
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
119
|
+
# property :age, from: :years, with: ->(value) { value.to_i }
|
|
120
|
+
# end
|
|
121
|
+
# klass.new(years: '37').age # => 37
|
|
74
122
|
def property(property_name, options = {})
|
|
75
123
|
super
|
|
76
124
|
|
|
@@ -79,7 +127,7 @@ module Hashie
|
|
|
79
127
|
transformer = options[:transform_with]
|
|
80
128
|
|
|
81
129
|
if from
|
|
82
|
-
fail_self_transformation_error!(property_name) if property_name == from
|
|
130
|
+
fail_self_transformation_error!(property_name) if property_name.to_s == from.to_s
|
|
83
131
|
define_translation(from, property_name, converter || transformer)
|
|
84
132
|
define_writer_for_source_property(from)
|
|
85
133
|
elsif valid_transformer?(transformer)
|
|
@@ -87,18 +135,44 @@ module Hashie
|
|
|
87
135
|
end
|
|
88
136
|
end
|
|
89
137
|
|
|
138
|
+
# @example Apply a declared transformer directly
|
|
139
|
+
# klass = Class.new(Hashie::Dash) do
|
|
140
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
141
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
142
|
+
# end
|
|
143
|
+
# klass.transformed_property(:age, '37') # => 37
|
|
90
144
|
def transformed_property(property_name, value)
|
|
91
145
|
transforms[property_name].call(value)
|
|
92
146
|
end
|
|
93
147
|
|
|
148
|
+
# @example Check for a property transformer
|
|
149
|
+
# klass = Class.new(Hashie::Dash) do
|
|
150
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
151
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
152
|
+
# end
|
|
153
|
+
# klass.transformation_exists?(:age) # => true
|
|
154
|
+
# klass.transformation_exists?(:name) # => false
|
|
94
155
|
def transformation_exists?(name)
|
|
95
156
|
transforms.key? name
|
|
96
157
|
end
|
|
97
158
|
|
|
159
|
+
# @example Check for a translated source key
|
|
160
|
+
# klass = Class.new(Hashie::Dash) do
|
|
161
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
162
|
+
# property :name, from: :fullName
|
|
163
|
+
# end
|
|
164
|
+
# klass.translation_exists?(:fullName) # => true
|
|
165
|
+
# klass.translation_exists?(:name) # => false
|
|
98
166
|
def translation_exists?(name)
|
|
99
167
|
translations_hash.key? name
|
|
100
168
|
end
|
|
101
169
|
|
|
170
|
+
# @example Map source keys to destination properties
|
|
171
|
+
# klass = Class.new(Hashie::Dash) do
|
|
172
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
173
|
+
# property :name, from: :fullName
|
|
174
|
+
# end
|
|
175
|
+
# klass.translations # => {fullName: :name}
|
|
102
176
|
def translations
|
|
103
177
|
@translations ||= {}.tap do |translations|
|
|
104
178
|
translations_hash.each do |(property_name, property_translations)|
|
|
@@ -112,6 +186,12 @@ module Hashie
|
|
|
112
186
|
end
|
|
113
187
|
end
|
|
114
188
|
|
|
189
|
+
# @example Map destination properties back to source keys
|
|
190
|
+
# klass = Class.new(Hashie::Dash) do
|
|
191
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
192
|
+
# property :name, from: :fullName
|
|
193
|
+
# end
|
|
194
|
+
# klass.inverse_translations # => {name: :fullName}
|
|
115
195
|
def inverse_translations
|
|
116
196
|
@inverse_translations ||= {}.tap do |translations|
|
|
117
197
|
translations_hash.each do |(property_name, property_translations)|
|
|
@@ -138,7 +218,7 @@ module Hashie
|
|
|
138
218
|
|
|
139
219
|
def fail_self_transformation_error!(property_name)
|
|
140
220
|
raise ArgumentError,
|
|
141
|
-
"
|
|
221
|
+
"property name `#{property_name}` and :from option must not be the same"
|
|
142
222
|
end
|
|
143
223
|
|
|
144
224
|
def valid_transformer?(transformer)
|
|
@@ -146,34 +226,77 @@ module Hashie
|
|
|
146
226
|
end
|
|
147
227
|
end
|
|
148
228
|
|
|
229
|
+
# Applies translations and transformations during assignment.
|
|
230
|
+
#
|
|
231
|
+
# @example
|
|
232
|
+
# klass = Class.new(Hashie::Dash) do
|
|
233
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
234
|
+
# property :name, from: :fullName
|
|
235
|
+
# end
|
|
236
|
+
# klass.new(fullName: 'Ada').name # => 'Ada'
|
|
149
237
|
module InstanceMethods
|
|
150
238
|
# Sets a value on the Dash in a Hash-like way.
|
|
151
239
|
#
|
|
152
240
|
# Note: Only works on pre-existing properties.
|
|
241
|
+
#
|
|
242
|
+
# @example Assign through a translated source key
|
|
243
|
+
# klass = Class.new(Hashie::Dash) do
|
|
244
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
245
|
+
# property :name, from: :fullName
|
|
246
|
+
# end
|
|
247
|
+
# dash = klass.new
|
|
248
|
+
# dash[:fullName] = 'Ada'
|
|
249
|
+
# dash.name # => 'Ada'
|
|
153
250
|
def []=(property, value)
|
|
154
251
|
if self.class.translation_exists? property
|
|
155
252
|
send("#{property}=", value)
|
|
156
|
-
|
|
253
|
+
|
|
254
|
+
if self.class.transformation_exists? property
|
|
255
|
+
super(property, self.class.transformed_property(property, value))
|
|
256
|
+
elsif self.class.properties.include?(property)
|
|
257
|
+
super
|
|
258
|
+
end
|
|
157
259
|
elsif self.class.transformation_exists? property
|
|
158
|
-
super
|
|
260
|
+
super(property, self.class.transformed_property(property, value))
|
|
159
261
|
elsif property_exists? property
|
|
160
262
|
super
|
|
161
263
|
end
|
|
162
264
|
end
|
|
163
265
|
|
|
164
266
|
# Deletes any keys that have a translation
|
|
267
|
+
#
|
|
268
|
+
# @example Initialize translated input without modifying the source
|
|
269
|
+
# klass = Class.new(Hashie::Dash) do
|
|
270
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
271
|
+
# property :name, from: :fullName
|
|
272
|
+
# end
|
|
273
|
+
# attributes = {fullName: 'Ada'}
|
|
274
|
+
# dash = klass.new
|
|
275
|
+
# dash.initialize_attributes(attributes)
|
|
276
|
+
# dash.name # => 'Ada'
|
|
277
|
+
# attributes # => {fullName: 'Ada'}
|
|
165
278
|
def initialize_attributes(attributes)
|
|
166
279
|
return unless attributes
|
|
280
|
+
|
|
167
281
|
attributes_copy = attributes.dup.delete_if do |k, v|
|
|
168
282
|
if self.class.translations_hash.include?(k)
|
|
169
283
|
self[k] = v
|
|
170
284
|
true
|
|
171
285
|
end
|
|
172
286
|
end
|
|
173
|
-
super
|
|
287
|
+
super(attributes_copy)
|
|
174
288
|
end
|
|
175
289
|
|
|
176
290
|
# Raises an NoMethodError if the property doesn't exist
|
|
291
|
+
#
|
|
292
|
+
# @example Check for an existing property
|
|
293
|
+
# klass = Class.new(Hashie::Dash) do
|
|
294
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
295
|
+
# property :name
|
|
296
|
+
# end
|
|
297
|
+
# dash = klass.new
|
|
298
|
+
# dash.property_exists?(:name) # => true
|
|
299
|
+
# expect { dash.property_exists?(:missing) }.to raise_error(NoMethodError)
|
|
177
300
|
def property_exists?(property)
|
|
178
301
|
fail_no_property_error!(property) unless self.class.property?(property)
|
|
179
302
|
true
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
|
-
# Searches a deeply nested
|
|
5
|
+
# Searches a deeply nested data structure for a key path, and returns the associated value.
|
|
4
6
|
#
|
|
5
7
|
# options = { user: { location: { address: '123 Street' } } }
|
|
6
8
|
# options.deep_fetch :user, :location, :address #=> '123 Street'
|
|
@@ -11,19 +13,29 @@ module Hashie
|
|
|
11
13
|
#
|
|
12
14
|
# This is particularly useful for fetching values from deeply nested api responses
|
|
13
15
|
# or params hashes.
|
|
16
|
+
#
|
|
17
|
+
# @example Traverse hashes and arrays
|
|
18
|
+
# hash = { users: [{ name: 'Ada' }] }.extend(Hashie::Extensions::DeepFetch)
|
|
19
|
+
# hash.deep_fetch(:users, 0, :name) # => 'Ada'
|
|
14
20
|
module DeepFetch
|
|
21
|
+
# @example Report an undefined path
|
|
22
|
+
# error = Hashie::Extensions::DeepFetch::UndefinedPathError.new('missing address')
|
|
23
|
+
# error.message # => 'missing address'
|
|
15
24
|
class UndefinedPathError < StandardError; end
|
|
16
25
|
|
|
26
|
+
# @example Use a fallback for a missing path
|
|
27
|
+
# hash = { user: {} }.extend(Hashie::Extensions::DeepFetch)
|
|
28
|
+
# hash.deep_fetch(:user, :name) { |key| "missing #{key}" } # => 'missing name'
|
|
29
|
+
# hash.deep_fetch(:user) # => {}
|
|
17
30
|
def deep_fetch(*args, &block)
|
|
18
31
|
args.reduce(self) do |obj, arg|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
32
|
+
arg = Integer(arg) if obj.is_a? Array
|
|
33
|
+
obj.fetch(arg)
|
|
34
|
+
rescue ArgumentError, IndexError, NoMethodError => e
|
|
35
|
+
break yield(arg) if block
|
|
36
|
+
|
|
37
|
+
raise UndefinedPathError,
|
|
38
|
+
"could not fetch path (`#{args.join(' > ')}`) at `#{arg}`", e.backtrace
|
|
27
39
|
end
|
|
28
40
|
end
|
|
29
41
|
end
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'hashie/extensions/deep_locate'
|
|
2
4
|
module Hashie
|
|
3
5
|
module Extensions
|
|
6
|
+
# @example Find values in nested collections
|
|
7
|
+
# hash = { users: [{ name: 'Ada' }] }.extend(Hashie::Extensions::DeepFind)
|
|
8
|
+
# hash.deep_find(:name) # => 'Ada'
|
|
4
9
|
module DeepFind
|
|
5
10
|
# Performs a depth-first search on deeply nested data structures for
|
|
6
11
|
# a key and returns the first occurrence of the key.
|
|
@@ -16,10 +21,17 @@ module Hashie
|
|
|
16
21
|
# my_hash = MyHash.new
|
|
17
22
|
# my_hash[:user] = {location: {address: '123 Street'}}
|
|
18
23
|
# my_hash.deep_find(:address) # => '123 Street'
|
|
24
|
+
#
|
|
25
|
+
# @example Return the first value
|
|
26
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
27
|
+
# hash.deep_find(:name) # => 'Ada'
|
|
19
28
|
def deep_find(key)
|
|
20
29
|
_deep_find(key)
|
|
21
30
|
end
|
|
22
31
|
|
|
32
|
+
# @example Detect the first nested value
|
|
33
|
+
# hash = { user: { name: 'Ada' } }.extend(Hashie::Extensions::DeepFind)
|
|
34
|
+
# hash.deep_detect(:name) # => 'Ada'
|
|
23
35
|
alias deep_detect deep_find
|
|
24
36
|
|
|
25
37
|
# Performs a depth-first search on deeply nested data structures for
|
|
@@ -44,11 +56,19 @@ module Hashie
|
|
|
44
56
|
# {location: {address: '234 Street'}}
|
|
45
57
|
# ]
|
|
46
58
|
# my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
|
|
59
|
+
#
|
|
60
|
+
# @example Collect all values, or nil if absent
|
|
61
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
62
|
+
# hash.deep_find_all(:name) # => ['Ada', 'Grace']
|
|
63
|
+
# hash.deep_find_all(:email) # => nil
|
|
47
64
|
def deep_find_all(key)
|
|
48
65
|
matches = _deep_find_all(key)
|
|
49
66
|
matches.empty? ? nil : matches
|
|
50
67
|
end
|
|
51
68
|
|
|
69
|
+
# @example Select every nested value
|
|
70
|
+
# hash = { users: [{ name: 'Ada' }, { name: 'Grace' }] }.extend(Hashie::Extensions::DeepFind)
|
|
71
|
+
# hash.deep_select(:name) # => ['Ada', 'Grace']
|
|
52
72
|
alias deep_select deep_find_all
|
|
53
73
|
|
|
54
74
|
private
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
# Searches nested hashes and arrays for matching string and symbol keys or values.
|
|
6
|
+
#
|
|
7
|
+
# @example Search a nested collection
|
|
8
|
+
# hash = { people: [{ name: 'Ada' }] }.extend(Hashie::Extensions::DeepGrep)
|
|
9
|
+
# hash.deep_grep(/Ada/) # => [{ name: 'Ada' }]
|
|
10
|
+
module DeepGrep
|
|
11
|
+
# @example Match a key and return its subtree
|
|
12
|
+
# hash = { user: { name: 'Ada' } }.extend(Hashie::Extensions::DeepGrep)
|
|
13
|
+
# hash.deep_grep(/user/) # => [{ user: { name: 'Ada' } }]
|
|
14
|
+
def deep_grep(pattern)
|
|
15
|
+
DeepGrep.deep_grep(pattern, self)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Returns key/subtree pairs for matching keys, containing hashes for matching
|
|
19
|
+
# scalar values, and matching scalar array elements. Matching keys stop descent.
|
|
20
|
+
#
|
|
21
|
+
# @example Search an array without extending it
|
|
22
|
+
# Hashie::Extensions::DeepGrep.deep_grep(/Ada/, ['Ada', 'Grace']) # => ['Ada']
|
|
23
|
+
def self.deep_grep(pattern, object)
|
|
24
|
+
raise ArgumentError, 'pattern must be a Regexp' unless pattern.is_a?(Regexp)
|
|
25
|
+
|
|
26
|
+
search(pattern, object, [], {}.compare_by_identity)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.search(pattern, object, results, ancestors)
|
|
30
|
+
return results if ancestors.key?(object) ||
|
|
31
|
+
!(object.is_a?(::Hash) || object.is_a?(::Array))
|
|
32
|
+
|
|
33
|
+
ancestors[object] = true
|
|
34
|
+
if object.is_a?(::Hash)
|
|
35
|
+
search_hash(pattern, object, results, ancestors)
|
|
36
|
+
else
|
|
37
|
+
search_array(pattern, object, results, ancestors)
|
|
38
|
+
end
|
|
39
|
+
ancestors.delete(object)
|
|
40
|
+
results
|
|
41
|
+
end
|
|
42
|
+
private_class_method :search
|
|
43
|
+
|
|
44
|
+
def self.search_hash(pattern, object, results, ancestors)
|
|
45
|
+
results << object if object.any? { |key, value| !matches?(pattern, key) && matches?(pattern, value) }
|
|
46
|
+
object.each do |key, value|
|
|
47
|
+
if matches?(pattern, key)
|
|
48
|
+
results << { key => value }
|
|
49
|
+
else
|
|
50
|
+
search(pattern, value, results, ancestors)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
private_class_method :search_hash
|
|
55
|
+
|
|
56
|
+
def self.search_array(pattern, object, results, ancestors)
|
|
57
|
+
object.each do |value|
|
|
58
|
+
matches?(pattern, value) ? results << value : search(pattern, value, results, ancestors)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
private_class_method :search_array
|
|
62
|
+
|
|
63
|
+
def self.matches?(pattern, value)
|
|
64
|
+
(value.is_a?(String) || value.is_a?(Symbol)) && pattern.match?(value.to_s)
|
|
65
|
+
end
|
|
66
|
+
private_class_method :matches?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,25 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
5
|
+
# @example Locate the hash containing a key
|
|
6
|
+
# Hashie::Extensions::DeepLocate.deep_locate(:name, { user: { name: 'Ada' } }) # => [{ name: 'Ada' }]
|
|
3
7
|
module DeepLocate
|
|
4
|
-
# The module level implementation of #deep_locate,
|
|
5
|
-
# to include/extend the base
|
|
8
|
+
# The module level implementation of #deep_locate, in case you do not want
|
|
9
|
+
# to include/extend the base data structure. For further examples please
|
|
6
10
|
# see #deep_locate.
|
|
7
11
|
#
|
|
8
12
|
# @example
|
|
9
|
-
# books = [
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
# pages: 120
|
|
13
|
-
# },
|
|
14
|
-
# ...
|
|
15
|
-
# ]
|
|
16
|
-
#
|
|
17
|
-
# DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
|
|
18
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
|
|
13
|
+
# books = [{ title: 'Ruby for beginners', pages: 120 }, { pages: 80 }]
|
|
14
|
+
# comparator = ->(key, _value, _object) { key == :title }
|
|
15
|
+
# Hashie::Extensions::DeepLocate.deep_locate(comparator, books) # => [{ title: 'Ruby for beginners', pages: 120 }]
|
|
19
16
|
def self.deep_locate(comparator, object)
|
|
20
|
-
unless comparator.respond_to?(:call)
|
|
21
|
-
comparator = _construct_key_comparator(comparator, object)
|
|
22
|
-
end
|
|
17
|
+
comparator = _construct_key_comparator(comparator, object) unless comparator.respond_to?(:call)
|
|
23
18
|
|
|
24
19
|
_deep_locate(comparator, object)
|
|
25
20
|
end
|
|
@@ -51,22 +46,16 @@ module Hashie
|
|
|
51
46
|
#
|
|
52
47
|
# books.extend(Hashie::Extensions::DeepLocate)
|
|
53
48
|
#
|
|
54
|
-
#
|
|
55
|
-
# #
|
|
56
|
-
#
|
|
57
|
-
# books.deep_locate
|
|
58
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
|
59
|
-
# # {:title=>"Ruby for the rest of us", :pages=>576}]
|
|
60
|
-
#
|
|
61
|
-
# books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
|
|
62
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
|
63
|
-
# # {:title=>"CSS for intermediates", :pages=>80}]
|
|
49
|
+
# ruby_titles = ->(key, value, _object) { key == :title && value.include?("Ruby") }
|
|
50
|
+
# books.deep_locate(ruby_titles) # => [{ title: 'Ruby for beginners', pages: 120 }, { title: 'Ruby for the rest of us', pages: 576 }]
|
|
51
|
+
# short_books = ->(key, value, _object) { key == :pages && value <= 120 }
|
|
52
|
+
# books.deep_locate(short_books) # => [{ title: 'Ruby for beginners', pages: 120 }, { title: 'CSS for intermediates', pages: 80 }]
|
|
64
53
|
def deep_locate(comparator)
|
|
65
54
|
Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
|
|
66
55
|
end
|
|
67
56
|
|
|
68
57
|
def self._construct_key_comparator(search_key, object)
|
|
69
|
-
if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
|
|
58
|
+
if (object.respond_to?(:indifferent_access?) && object.indifferent_access?) ||
|
|
70
59
|
activesupport_indifferent?(object)
|
|
71
60
|
search_key = search_key.to_s
|
|
72
61
|
end
|
|
@@ -79,9 +68,7 @@ module Hashie
|
|
|
79
68
|
|
|
80
69
|
def self._deep_locate(comparator, object, result = [])
|
|
81
70
|
if object.is_a?(::Enumerable)
|
|
82
|
-
if object.any? { |value| _match_comparator?(value, comparator, object) }
|
|
83
|
-
result.push object
|
|
84
|
-
end
|
|
71
|
+
result.push object if object.any? { |value| _match_comparator?(value, comparator, object) }
|
|
85
72
|
|
|
86
73
|
(object.respond_to?(:values) ? object.values : object.entries).each do |value|
|
|
87
74
|
_deep_locate(comparator, value, result)
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
5
|
+
# @example Merge nested hashes
|
|
6
|
+
# hash = { user: { name: 'Ada' } }.extend(Hashie::Extensions::DeepMerge)
|
|
7
|
+
# hash.deep_merge(user: { active: true }) # => { user: { name: 'Ada', active: true } }
|
|
3
8
|
module DeepMerge
|
|
4
9
|
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
|
10
|
+
#
|
|
11
|
+
# @example Merge without changing the receiver
|
|
12
|
+
# hash = { settings: { count: 1 } }.extend(Hashie::Extensions::DeepMerge)
|
|
13
|
+
# hash.deep_merge(settings: { count: 2 }) # => { settings: { count: 2 } }
|
|
14
|
+
# hash # => { settings: { count: 1 } }
|
|
5
15
|
def deep_merge(other_hash, &block)
|
|
6
16
|
copy = _deep_dup(self)
|
|
7
17
|
copy.extend(Hashie::Extensions::DeepMerge) unless copy.respond_to?(:deep_merge!)
|
|
@@ -10,8 +20,14 @@ module Hashie
|
|
|
10
20
|
|
|
11
21
|
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
|
12
22
|
# Modifies the receiver in place.
|
|
23
|
+
#
|
|
24
|
+
# @example Resolve a nested conflict with a block
|
|
25
|
+
# hash = { settings: { count: 1 } }.extend(Hashie::Extensions::DeepMerge)
|
|
26
|
+
# hash.deep_merge!({ settings: { count: 2 } }) { |_key, old, new| old + new }
|
|
27
|
+
# hash # => { settings: { count: 3 } }
|
|
13
28
|
def deep_merge!(other_hash, &block)
|
|
14
29
|
return self unless other_hash.is_a?(::Hash)
|
|
30
|
+
|
|
15
31
|
_recursive_merge(self, other_hash, &block)
|
|
16
32
|
self
|
|
17
33
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
# Initializes a Hash subclass, converting nested hashes to the same class.
|
|
6
|
+
#
|
|
7
|
+
# @example Initialize nested hash instances
|
|
8
|
+
# class NestedHash < Hash
|
|
9
|
+
# include Hashie::Extensions::DeepMergeInitializer
|
|
10
|
+
# end
|
|
11
|
+
# hash = NestedHash.new(user: { name: 'Ada' })
|
|
12
|
+
# hash[:user].class # => NestedHash
|
|
13
|
+
module DeepMergeInitializer
|
|
14
|
+
# @example Convert hashes inside arrays and retain a default
|
|
15
|
+
# class NestedHash < Hash
|
|
16
|
+
# include Hashie::Extensions::DeepMergeInitializer
|
|
17
|
+
# end
|
|
18
|
+
# hash = NestedHash.new({ users: [{ name: 'Ada' }] }, false)
|
|
19
|
+
# hash[:users].first.class # => NestedHash
|
|
20
|
+
# hash[:users].first[:missing] # => false
|
|
21
|
+
def initialize(hash = {}, default = nil, &block)
|
|
22
|
+
default.nil? ? super(&block) : super(default)
|
|
23
|
+
hash.each_pair do |key, value|
|
|
24
|
+
self[key] = deep_merge_initializer_value(value, default, &block)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def deep_merge_initializer_value(value, default, &block)
|
|
31
|
+
case value
|
|
32
|
+
when ::Hash
|
|
33
|
+
self.class.new(value, default, &block)
|
|
34
|
+
when ::Array
|
|
35
|
+
value.map { |item| deep_merge_initializer_value(item, default, &block) }
|
|
36
|
+
else
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
# IgnoreUndeclared is a simple mixin that silently ignores
|
|
@@ -10,7 +12,7 @@ module Hashie
|
|
|
10
12
|
# that property was provided at initialization.
|
|
11
13
|
#
|
|
12
14
|
# @example
|
|
13
|
-
# class Person < Trash
|
|
15
|
+
# class Person < Hashie::Trash
|
|
14
16
|
# include Hashie::Extensions::IgnoreUndeclared
|
|
15
17
|
#
|
|
16
18
|
# property :first_name
|
|
@@ -27,18 +29,33 @@ module Hashie
|
|
|
27
29
|
#
|
|
28
30
|
# p.first_name # => 'Freddy'
|
|
29
31
|
# p.last_name # => 'Nostrils'
|
|
30
|
-
# p.email
|
|
32
|
+
# expect { p.email }.to raise_error(NoMethodError)
|
|
31
33
|
module IgnoreUndeclared
|
|
34
|
+
# @example Initialize only declared attributes
|
|
35
|
+
# class Person < Hashie::Trash
|
|
36
|
+
# include Hashie::Extensions::IgnoreUndeclared
|
|
37
|
+
# property :name
|
|
38
|
+
# end
|
|
39
|
+
# person = Person.new
|
|
40
|
+
# person.initialize_attributes(name: 'Ada', email: 'ada@example.com')
|
|
41
|
+
# person.to_h # => { name: 'Ada' }
|
|
32
42
|
def initialize_attributes(attributes)
|
|
33
43
|
return unless attributes
|
|
44
|
+
|
|
34
45
|
klass = self.class
|
|
35
|
-
translations = klass.respond_to?(:translations) && klass.translations
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self[att] = value
|
|
39
|
-
end
|
|
46
|
+
translations = (klass.respond_to?(:translations) && klass.translations) || []
|
|
47
|
+
|
|
48
|
+
super(attributes.select { |attr, _| klass.property?(attr) || translations.include?(attr) })
|
|
40
49
|
end
|
|
41
50
|
|
|
51
|
+
# @example Check the declaration rather than the current value
|
|
52
|
+
# class Person < Hashie::Trash
|
|
53
|
+
# include Hashie::Extensions::IgnoreUndeclared
|
|
54
|
+
# property :name
|
|
55
|
+
# end
|
|
56
|
+
# person = Person.new
|
|
57
|
+
# person.property_exists?(:name) # => true
|
|
58
|
+
# person.property_exists?(:email) # => false
|
|
42
59
|
def property_exists?(property)
|
|
43
60
|
self.class.property?(property)
|
|
44
61
|
end
|