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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. 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
- # == Example from inconsistent APIs
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
- # == Example using translation lambdas
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 (Fixnum if you are using Ruby 2.3 or lower)
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
- # * <tt>:default</tt> - The class inheriting the translations.
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.dup)
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
- # * <tt>:default</tt> - Specify a default value for this property, to be
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
- # * <tt>:from</tt> - Specify the original key name that will be write only.
71
- # * <tt>:with</tt> - Specify a lambda to be used to convert value.
72
- # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
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
- "Property name (#{property_name}) and :from option must not be the same"
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
- super(property, value) if self.class.properties.include?(property)
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 property, self.class.transformed_property(property, value)
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 attributes_copy
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 datastructure for a key path, and returns the associated value.
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
- begin
20
- arg = Integer(arg) if obj.is_a? Array
21
- obj.fetch(arg)
22
- rescue ArgumentError, IndexError, NoMethodError => e
23
- break yield(arg) if block
24
- raise UndefinedPathError,
25
- "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
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, incase you do not want
5
- # to include/extend the base datastructure. For further examples please
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
- # title: "Ruby for beginners",
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
- # # for ruby 1.9 leave *no* space between the lambda rocket and the braces
55
- # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
56
- #
57
- # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
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 # => NoMethodError
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
- attributes.each_pair do |att, value|
37
- next unless klass.property?(att) || (translations && translations.include?(att))
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