hashie 2.1.2 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +524 -59
  3. data/CONTRIBUTING.md +24 -7
  4. data/README.md +781 -90
  5. data/Rakefile +19 -2
  6. data/UPGRADING.md +245 -0
  7. data/hashie.gemspec +21 -13
  8. data/lib/hashie.rb +60 -21
  9. data/lib/hashie/array.rb +21 -0
  10. data/lib/hashie/clash.rb +24 -12
  11. data/lib/hashie/dash.rb +96 -33
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
  14. data/lib/hashie/extensions/coercion.rb +124 -18
  15. data/lib/hashie/extensions/dash/coercion.rb +25 -0
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +56 -0
  17. data/lib/hashie/extensions/dash/property_translation.rb +191 -0
  18. data/lib/hashie/extensions/deep_fetch.rb +7 -5
  19. data/lib/hashie/extensions/deep_find.rb +69 -0
  20. data/lib/hashie/extensions/deep_locate.rb +113 -0
  21. data/lib/hashie/extensions/deep_merge.rb +35 -12
  22. data/lib/hashie/extensions/ignore_undeclared.rb +11 -5
  23. data/lib/hashie/extensions/indifferent_access.rb +28 -16
  24. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  25. data/lib/hashie/extensions/key_conversion.rb +0 -82
  26. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  27. data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
  28. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  29. data/lib/hashie/extensions/mash/safe_assignment.rb +18 -0
  30. data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
  31. data/lib/hashie/extensions/method_access.rb +154 -11
  32. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +48 -0
  33. data/lib/hashie/extensions/pretty_inspect.rb +19 -0
  34. data/lib/hashie/extensions/ruby_version.rb +60 -0
  35. data/lib/hashie/extensions/ruby_version_check.rb +21 -0
  36. data/lib/hashie/extensions/strict_key_access.rb +77 -0
  37. data/lib/hashie/extensions/stringify_keys.rb +71 -0
  38. data/lib/hashie/extensions/symbolize_keys.rb +71 -0
  39. data/lib/hashie/hash.rb +27 -8
  40. data/lib/hashie/logger.rb +18 -0
  41. data/lib/hashie/mash.rb +235 -57
  42. data/lib/hashie/railtie.rb +21 -0
  43. data/lib/hashie/rash.rb +40 -16
  44. data/lib/hashie/trash.rb +2 -88
  45. data/lib/hashie/utils.rb +44 -0
  46. data/lib/hashie/version.rb +1 -1
  47. metadata +42 -81
  48. data/.gitignore +0 -9
  49. data/.rspec +0 -2
  50. data/.rubocop.yml +0 -36
  51. data/.travis.yml +0 -15
  52. data/Gemfile +0 -11
  53. data/Guardfile +0 -5
  54. data/lib/hashie/hash_extensions.rb +0 -47
  55. data/spec/hashie/clash_spec.rb +0 -48
  56. data/spec/hashie/dash_spec.rb +0 -338
  57. data/spec/hashie/extensions/coercion_spec.rb +0 -156
  58. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -70
  59. data/spec/hashie/extensions/deep_merge_spec.rb +0 -22
  60. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -23
  61. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -152
  62. data/spec/hashie/extensions/key_conversion_spec.rb +0 -103
  63. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  64. data/spec/hashie/extensions/method_access_spec.rb +0 -121
  65. data/spec/hashie/hash_spec.rb +0 -66
  66. data/spec/hashie/mash_spec.rb +0 -467
  67. data/spec/hashie/rash_spec.rb +0 -44
  68. data/spec/hashie/trash_spec.rb +0 -193
  69. data/spec/hashie/version_spec.rb +0 -7
  70. data/spec/spec.opts +0 -3
  71. data/spec/spec_helper.rb +0 -8
@@ -0,0 +1,21 @@
1
+ require 'hashie/extensions/ruby_version'
2
+
3
+ module Hashie
4
+ module Extensions
5
+ module RubyVersionCheck
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def with_minimum_ruby(version)
12
+ yield if with_minimum_ruby?(version)
13
+ end
14
+
15
+ def with_minimum_ruby?(version)
16
+ RubyVersion.new(RUBY_VERSION) >= RubyVersion.new(version)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ module Hashie
2
+ module Extensions
3
+ # SRP: This extension will fail an error whenever a key is accessed
4
+ # that does not exist in the hash.
5
+ #
6
+ # EXAMPLE:
7
+ #
8
+ # class StrictKeyAccessHash < Hash
9
+ # include Hashie::Extensions::StrictKeyAccess
10
+ # end
11
+ #
12
+ # >> hash = StrictKeyAccessHash[foo: "bar"]
13
+ # => {:foo=>"bar"}
14
+ # >> hash[:foo]
15
+ # => "bar"
16
+ # >> hash[:cow]
17
+ # KeyError: key not found: :cow
18
+ #
19
+ # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash
20
+ # behave more like a "Dictionary".
21
+ #
22
+ module StrictKeyAccess
23
+ class DefaultError < StandardError
24
+ def initialize
25
+ super('Setting or using a default with Hashie::Extensions::StrictKeyAccess'\
26
+ ' does not make sense'
27
+ )
28
+ end
29
+ end
30
+
31
+ # NOTE: Defaults don't make any sense with a StrictKeyAccess.
32
+ # NOTE: When key lookup fails a KeyError is raised.
33
+ #
34
+ # Normal:
35
+ #
36
+ # >> a = Hash.new(123)
37
+ # => {}
38
+ # >> a["noes"]
39
+ # => 123
40
+ #
41
+ # With StrictKeyAccess:
42
+ #
43
+ # >> a = StrictKeyAccessHash.new(123)
44
+ # => {}
45
+ # >> a["noes"]
46
+ # KeyError: key not found: "noes"
47
+ #
48
+ def [](key)
49
+ fetch(key)
50
+ end
51
+
52
+ def default(_ = nil)
53
+ raise DefaultError
54
+ end
55
+
56
+ def default=(_)
57
+ raise DefaultError
58
+ end
59
+
60
+ def default_proc
61
+ raise DefaultError
62
+ end
63
+
64
+ def default_proc=(_)
65
+ raise DefaultError
66
+ end
67
+
68
+ def key(value)
69
+ super.tap do |result|
70
+ if result.nil? && (!key?(result) || self[result] != value)
71
+ raise KeyError, "key not found with value of #{value.inspect}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,71 @@
1
+ module Hashie
2
+ module Extensions
3
+ module StringifyKeys
4
+ # Convert all keys in the hash to strings.
5
+ #
6
+ # @example
7
+ # test = {:abc => 'def'}
8
+ # test.stringify_keys!
9
+ # test # => {'abc' => 'def'}
10
+ def stringify_keys!
11
+ StringifyKeys.stringify_keys!(self)
12
+ self
13
+ end
14
+
15
+ # Return a new hash with all keys converted
16
+ # to strings.
17
+ def stringify_keys
18
+ StringifyKeys.stringify_keys(self)
19
+ end
20
+
21
+ module ClassMethods
22
+ # Stringify all keys recursively within nested
23
+ # hashes and arrays.
24
+ # @api private
25
+ def stringify_keys_recursively!(object)
26
+ case object
27
+ when self.class
28
+ stringify_keys!(object)
29
+ when ::Array
30
+ object.each do |i|
31
+ stringify_keys_recursively!(i)
32
+ end
33
+ when ::Hash
34
+ stringify_keys!(object)
35
+ end
36
+ end
37
+
38
+ # Convert all keys in the hash to strings.
39
+ #
40
+ # @param [::Hash] hash
41
+ # @example
42
+ # test = {:abc => 'def'}
43
+ # test.stringify_keys!
44
+ # test # => {'abc' => 'def'}
45
+ def stringify_keys!(hash)
46
+ hash.extend(Hashie::Extensions::StringifyKeys) unless hash.respond_to?(:stringify_keys!)
47
+ hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
48
+ stringify_keys_recursively!(hash[k])
49
+ hash[k.to_s] = hash.delete(k)
50
+ end
51
+ hash
52
+ end
53
+
54
+ # Return a copy of hash with all keys converted
55
+ # to strings.
56
+ # @param [::Hash] hash
57
+ def stringify_keys(hash)
58
+ copy = hash.dup
59
+ copy.extend(Hashie::Extensions::StringifyKeys) unless copy.respond_to?(:stringify_keys!)
60
+ copy.tap do |new_hash|
61
+ stringify_keys!(new_hash)
62
+ end
63
+ end
64
+ end
65
+
66
+ class << self
67
+ include ClassMethods
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,71 @@
1
+ module Hashie
2
+ module Extensions
3
+ module SymbolizeKeys
4
+ # Convert all keys in the hash to symbols.
5
+ #
6
+ # @example
7
+ # test = {'abc' => 'def'}
8
+ # test.symbolize_keys!
9
+ # test # => {:abc => 'def'}
10
+ def symbolize_keys!
11
+ SymbolizeKeys.symbolize_keys!(self)
12
+ self
13
+ end
14
+
15
+ # Return a new hash with all keys converted
16
+ # to symbols.
17
+ def symbolize_keys
18
+ SymbolizeKeys.symbolize_keys(self)
19
+ end
20
+
21
+ module ClassMethods
22
+ # Symbolize all keys recursively within nested
23
+ # hashes and arrays.
24
+ # @api private
25
+ def symbolize_keys_recursively!(object)
26
+ case object
27
+ when self.class
28
+ symbolize_keys!(object)
29
+ when ::Array
30
+ object.each do |i|
31
+ symbolize_keys_recursively!(i)
32
+ end
33
+ when ::Hash
34
+ symbolize_keys!(object)
35
+ end
36
+ end
37
+
38
+ # Convert all keys in hash to symbols.
39
+ #
40
+ # @param [Hash] hash
41
+ # @example
42
+ # test = {'abc' => 'def'}
43
+ # Hashie.symbolize_keys! test
44
+ # test # => {:abc => 'def'}
45
+ def symbolize_keys!(hash)
46
+ hash.extend(Hashie::Extensions::SymbolizeKeys) unless hash.respond_to?(:symbolize_keys!)
47
+ hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
48
+ symbolize_keys_recursively!(hash[k])
49
+ hash[k.to_sym] = hash.delete(k)
50
+ end
51
+ hash
52
+ end
53
+
54
+ # Return a copy of hash with all keys converted
55
+ # to symbols.
56
+ # @param [::Hash] hash
57
+ def symbolize_keys(hash)
58
+ copy = hash.dup
59
+ copy.extend(Hashie::Extensions::SymbolizeKeys) unless copy.respond_to?(:symbolize_keys!)
60
+ copy.tap do |new_hash|
61
+ symbolize_keys!(new_hash)
62
+ end
63
+ end
64
+ end
65
+
66
+ class << self
67
+ include ClassMethods
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,25 +1,38 @@
1
- require 'hashie/hash_extensions'
1
+ require 'hashie/extensions/stringify_keys'
2
+ require 'hashie/extensions/pretty_inspect'
2
3
 
3
4
  module Hashie
4
5
  # A Hashie Hash is simply a Hash that has convenience
5
6
  # functions baked in such as stringify_keys that may
6
7
  # not be available in all libraries.
7
8
  class Hash < ::Hash
8
- include HashExtensions
9
+ include Hashie::Extensions::PrettyInspect
10
+ include Hashie::Extensions::StringifyKeys
11
+
12
+ # Convert this hash into a Mash
13
+ def to_mash
14
+ ::Hashie::Mash.new(self)
15
+ end
9
16
 
10
17
  # Converts a mash back to a hash (with stringified or symbolized keys)
11
18
  def to_hash(options = {})
12
19
  out = {}
13
- keys.each do |k|
14
- assignment_key = k.to_s
15
- assignment_key = assignment_key.to_sym if options[:symbolize_keys]
20
+ each_key do |k|
21
+ assignment_key =
22
+ if options[:stringify_keys]
23
+ k.to_s
24
+ elsif options[:symbolize_keys]
25
+ k.to_s.to_sym
26
+ else
27
+ k
28
+ end
16
29
  if self[k].is_a?(Array)
17
30
  out[assignment_key] ||= []
18
31
  self[k].each do |array_object|
19
- out[assignment_key] << (Hash === array_object ? flexibly_convert_to_hash(array_object, options) : array_object)
32
+ out[assignment_key] << maybe_convert_to_hash(array_object, options)
20
33
  end
21
34
  else
22
- out[assignment_key] = (Hash === self[k] || self[k].respond_to?(:to_hash)) ? flexibly_convert_to_hash(self[k], options) : self[k]
35
+ out[assignment_key] = maybe_convert_to_hash(self[k], options)
23
36
  end
24
37
  end
25
38
  out
@@ -32,8 +45,14 @@ module Hashie
32
45
 
33
46
  private
34
47
 
48
+ def maybe_convert_to_hash(object, options)
49
+ return object unless object.is_a?(Hash) || object.respond_to?(:to_hash)
50
+
51
+ flexibly_convert_to_hash(object, options)
52
+ end
53
+
35
54
  def flexibly_convert_to_hash(object, options = {})
36
- if object.method(:to_hash).arity == 0
55
+ if object.method(:to_hash).arity.zero?
37
56
  object.to_hash
38
57
  else
39
58
  object.to_hash(options)
@@ -0,0 +1,18 @@
1
+ require 'logger'
2
+
3
+ module Hashie
4
+ # The logger that Hashie uses for reporting errors.
5
+ #
6
+ # @return [Logger]
7
+ def self.logger
8
+ @logger ||= Logger.new(STDOUT)
9
+ end
10
+
11
+ # Sets the logger that Hashie uses for reporting errors.
12
+ #
13
+ # @param logger [Logger] The logger to set as Hashie's logger.
14
+ # @return [void]
15
+ def self.logger=(logger)
16
+ @logger = logger
17
+ end
18
+ end
@@ -1,4 +1,8 @@
1
1
  require 'hashie/hash'
2
+ require 'hashie/array'
3
+ require 'hashie/utils'
4
+ require 'hashie/logger'
5
+ require 'hashie/extensions/key_conflict_warning'
2
6
 
3
7
  module Hashie
4
8
  # Mash allows you to create pseudo-objects that have method-like
@@ -12,9 +16,12 @@ module Hashie
12
16
  #
13
17
  # * No punctuation: Returns the value of the hash for that key, or nil if none exists.
14
18
  # * Assignment (<tt>=</tt>): Sets the attribute of the given method name.
15
- # * Existence (<tt>?</tt>): Returns true or false depending on whether that key has been set.
16
- # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it as "touch" for mashes.
17
- # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key. Used to test existance in deep Mashes.
19
+ # * Truthiness (<tt>?</tt>): Returns true or false depending on the truthiness of
20
+ # the attribute, or false if the key is not set.
21
+ # * Bang (<tt>!</tt>): Forces the existence of this key, used for deep Mashes. Think of it
22
+ # as "touch" for mashes.
23
+ # * Under Bang (<tt>_</tt>): Like Bang, but returns a new Mash rather than creating a key.
24
+ # Used to test existance in deep Mashes.
18
25
  #
19
26
  # == Basic Example
20
27
  #
@@ -55,9 +62,37 @@ module Hashie
55
62
  # mash.author # => <Mash>
56
63
  #
57
64
  class Mash < Hash
58
- ALLOWED_SUFFIXES = %w(? ! = _)
59
- include Hashie::PrettyInspect
60
- alias_method :to_s, :inspect
65
+ include Hashie::Extensions::PrettyInspect
66
+ include Hashie::Extensions::RubyVersionCheck
67
+ extend Hashie::Extensions::KeyConflictWarning
68
+
69
+ ALLOWED_SUFFIXES = %w[? ! = _].freeze
70
+
71
+ def self.load(path, options = {})
72
+ @_mashes ||= new
73
+
74
+ return @_mashes[path] if @_mashes.key?(path)
75
+ raise ArgumentError, "The following file doesn't exist: #{path}" unless File.file?(path)
76
+
77
+ options = options.dup
78
+ parser = options.delete(:parser) { Hashie::Extensions::Parsers::YamlErbParser }
79
+ @_mashes[path] = new(parser.perform(path, options)).freeze
80
+ end
81
+
82
+ def to_module(mash_method_name = :settings)
83
+ mash = self
84
+ Module.new do |m|
85
+ m.send :define_method, mash_method_name.to_sym do
86
+ mash
87
+ end
88
+ end
89
+ end
90
+
91
+ def with_accessors!
92
+ extend Hashie::Extensions::Mash::DefineAccessors
93
+ end
94
+
95
+ alias to_s inspect
61
96
 
62
97
  # If you pass in an existing hash, it will
63
98
  # convert it to a Mash including recursively
@@ -68,22 +103,28 @@ module Hashie
68
103
  default ? super(default) : super(&blk)
69
104
  end
70
105
 
71
- class << self; alias_method :[], :new; end
72
-
73
- def id #:nodoc:
74
- self['id']
106
+ # Creates a new anonymous subclass with key conflict
107
+ # warnings disabled. You may pass an array of method
108
+ # symbols to restrict the disabled warnings to.
109
+ # Hashie::Mash.quiet.new(hash) all warnings disabled.
110
+ # Hashie::Mash.quiet(:zip).new(hash) only zip warning
111
+ # is disabled.
112
+ def self.quiet(*method_keys)
113
+ @memoized_classes ||= {}
114
+ @memoized_classes[method_keys] ||= Class.new(self) do
115
+ disable_warnings(*method_keys)
116
+ end
75
117
  end
76
118
 
77
- def type #:nodoc:
78
- self['type']
79
- end
119
+ class << self; alias [] new; end
80
120
 
81
- alias_method :regular_reader, :[]
82
- alias_method :regular_writer, :[]=
121
+ alias regular_reader []
122
+ alias regular_writer []=
83
123
 
84
124
  # Retrieves an attribute set in the Mash. Will convert
85
125
  # any key passed in to a string before retrieving.
86
126
  def custom_reader(key)
127
+ default_proc.call(self, key) if default_proc && !key?(key)
87
128
  value = regular_reader(convert_key(key))
88
129
  yield value if block_given?
89
130
  value
@@ -93,11 +134,14 @@ module Hashie
93
134
  # a string before it is set, and Hashes will be converted
94
135
  # into Mashes for nesting purposes.
95
136
  def custom_writer(key, value, convert = true) #:nodoc:
96
- regular_writer(convert_key(key), convert ? convert_value(value) : value)
137
+ key_as_symbol = (key = convert_key(key)).to_sym
138
+
139
+ log_built_in_message(key_as_symbol) if log_collision?(key_as_symbol)
140
+ regular_writer(key, convert ? convert_value(value) : value)
97
141
  end
98
142
 
99
- alias_method :[], :custom_reader
100
- alias_method :[]=, :custom_writer
143
+ alias [] custom_reader
144
+ alias []= custom_writer
101
145
 
102
146
  # This is the bang method reader, it will return a new Mash
103
147
  # if there isn't a value already assigned to the key requested.
@@ -126,44 +170,98 @@ module Hashie
126
170
  super(convert_key(key))
127
171
  end
128
172
 
129
- alias_method :regular_dup, :dup
173
+ def values_at(*keys)
174
+ super(*keys.map { |key| convert_key(key) })
175
+ end
176
+
177
+ # Returns a new instance of the class it was called on, using its keys as
178
+ # values, and its values as keys. The new values and keys will always be
179
+ # strings.
180
+ def invert
181
+ self.class.new(super)
182
+ end
183
+
184
+ # Returns a new instance of the class it was called on, containing elements
185
+ # for which the given block returns false.
186
+ def reject(&blk)
187
+ self.class.new(super(&blk))
188
+ end
189
+
190
+ # Returns a new instance of the class it was called on, containing elements
191
+ # for which the given block returns true.
192
+ def select(&blk)
193
+ self.class.new(super(&blk))
194
+ end
195
+
196
+ alias regular_dup dup
130
197
  # Duplicates the current mash as a new mash.
131
198
  def dup
132
- self.class.new(self, default)
199
+ self.class.new(self, default, &default_proc)
133
200
  end
134
201
 
202
+ alias regular_key? key?
135
203
  def key?(key)
136
204
  super(convert_key(key))
137
205
  end
138
- alias_method :has_key?, :key?
139
- alias_method :include?, :key?
140
- alias_method :member?, :key?
206
+ alias has_key? key?
207
+ alias include? key?
208
+ alias member? key?
209
+
210
+ if with_minimum_ruby?('2.6.0')
211
+ # Performs a deep_update on a duplicate of the
212
+ # current mash.
213
+ def deep_merge(*other_hashes, &blk)
214
+ dup.deep_update(*other_hashes, &blk)
215
+ end
216
+
217
+ # Recursively merges this mash with the passed
218
+ # in hash, merging each hash in the hierarchy.
219
+ def deep_update(*other_hashes, &blk)
220
+ other_hashes.each do |other_hash|
221
+ _deep_update(other_hash, &blk)
222
+ end
223
+ self
224
+ end
225
+ else
226
+ # Performs a deep_update on a duplicate of the
227
+ # current mash.
228
+ def deep_merge(other_hash, &blk)
229
+ dup.deep_update(other_hash, &blk)
230
+ end
141
231
 
142
- # Performs a deep_update on a duplicate of the
143
- # current mash.
144
- def deep_merge(other_hash, &blk)
145
- dup.deep_update(other_hash, &blk)
232
+ # Recursively merges this mash with the passed
233
+ # in hash, merging each hash in the hierarchy.
234
+ def deep_update(other_hash, &blk)
235
+ _deep_update(other_hash, &blk)
236
+ self
237
+ end
146
238
  end
147
- alias_method :merge, :deep_merge
148
239
 
149
- # Recursively merges this mash with the passed
150
- # in hash, merging each hash in the hierarchy.
151
- def deep_update(other_hash, &blk)
240
+ # Alias these lexically so they get the correctly defined
241
+ # #deep_merge and #deep_update based on ruby version.
242
+ alias merge deep_merge
243
+ alias deep_merge! deep_update
244
+ alias update deep_update
245
+ alias merge! update
246
+
247
+ def _deep_update(other_hash, &blk)
152
248
  other_hash.each_pair do |k, v|
153
249
  key = convert_key(k)
154
- if regular_reader(key).is_a?(Mash) && v.is_a?(::Hash)
250
+ if v.is_a?(::Hash) && key?(key) && regular_reader(key).is_a?(Mash)
155
251
  custom_reader(key).deep_update(v, &blk)
156
252
  else
157
253
  value = convert_value(v, true)
158
- value = convert_value(blk.call(key, self[k], value), true) if blk
254
+ value = convert_value(yield(key, self[k], value), true) if blk && key?(k)
159
255
  custom_writer(key, value, false)
160
256
  end
161
257
  end
162
- self
163
258
  end
164
- alias_method :deep_merge!, :deep_update
165
- alias_method :update, :deep_update
166
- alias_method :merge!, :update
259
+ private :_deep_update
260
+
261
+ # Assigns a value to a key
262
+ def assign_property(name, value)
263
+ self[name] = value
264
+ end
167
265
 
168
266
  # Performs a shallow_update on a duplicate of the current mash
169
267
  def shallow_merge(other_hash)
@@ -185,11 +283,14 @@ module Hashie
185
283
  self
186
284
  end
187
285
 
188
- # Will return true if the Mash has had a key
189
- # set in addition to normal respond_to? functionality.
190
- def respond_to?(method_name, include_private = false)
191
- return true if key?(method_name) || prefix_method?(method_name)
192
- super
286
+ def respond_to_missing?(method_name, *args)
287
+ return true if key?(method_name)
288
+ suffix = method_suffix(method_name)
289
+ if suffix
290
+ true
291
+ else
292
+ super
293
+ end
193
294
  end
194
295
 
195
296
  def prefix_method?(method_name)
@@ -197,26 +298,78 @@ module Hashie
197
298
  method_name.end_with?(*ALLOWED_SUFFIXES) && key?(method_name.chop)
198
299
  end
199
300
 
200
- def method_missing(method_name, *args, &blk)
301
+ def method_missing(method_name, *args, &blk) # rubocop:disable Style/MethodMissing
201
302
  return self.[](method_name, &blk) if key?(method_name)
202
- suffixes_regex = ALLOWED_SUFFIXES.join
203
- match = method_name.to_s.match(/(.*?)([#{suffixes_regex}]?)$/)
204
- case match[2]
205
- when '='
206
- self[match[1]] = args.first
207
- when '?'
208
- !!self[match[1]]
209
- when '!'
210
- initializing_reader(match[1])
211
- when '_'
212
- underbang_reader(match[1])
303
+ name, suffix = method_name_and_suffix(method_name)
304
+ case suffix
305
+ when '='.freeze
306
+ assign_property(name, args.first)
307
+ when '?'.freeze
308
+ !!self[name]
309
+ when '!'.freeze
310
+ initializing_reader(name)
311
+ when '_'.freeze
312
+ underbang_reader(name)
213
313
  else
214
- default(method_name)
314
+ self[method_name]
315
+ end
316
+ end
317
+
318
+ # play nice with ActiveSupport Array#extract_options!
319
+ def extractable_options?
320
+ true
321
+ end
322
+
323
+ # another ActiveSupport method, see issue #270
324
+ def reverse_merge(other_hash)
325
+ self.class.new(other_hash).merge(self)
326
+ end
327
+
328
+ with_minimum_ruby('2.3.0') do
329
+ def dig(*keys)
330
+ super(*keys.map { |key| convert_key(key) })
331
+ end
332
+ end
333
+
334
+ with_minimum_ruby('2.4.0') do
335
+ def transform_values(&blk)
336
+ self.class.new(super(&blk))
337
+ end
338
+
339
+ # Returns a new instance of the class it was called on, with nil values
340
+ # removed.
341
+ def compact
342
+ self.class.new(super)
343
+ end
344
+ end
345
+
346
+ with_minimum_ruby('2.5.0') do
347
+ def slice(*keys)
348
+ string_keys = keys.map { |key| convert_key(key) }
349
+ self.class.new(super(*string_keys))
350
+ end
351
+
352
+ def transform_keys(&blk)
353
+ self.class.new(super(&blk))
215
354
  end
216
355
  end
217
356
 
218
357
  protected
219
358
 
359
+ def method_name_and_suffix(method_name)
360
+ method_name = method_name.to_s
361
+ if method_name.end_with?(*ALLOWED_SUFFIXES)
362
+ [method_name[0..-2], method_name[-1]]
363
+ else
364
+ [method_name[0..-1], nil]
365
+ end
366
+ end
367
+
368
+ def method_suffix(method_name)
369
+ method_name = method_name.to_s
370
+ method_name[-1] if method_name.end_with?(*ALLOWED_SUFFIXES)
371
+ end
372
+
220
373
  def convert_key(key) #:nodoc:
221
374
  key.to_s
222
375
  end
@@ -230,11 +383,36 @@ module Hashie
230
383
  when ::Hash
231
384
  val = val.dup if duping
232
385
  self.class.new(val)
233
- when Array
234
- val.map { |e| convert_value(e) }
386
+ when ::Array
387
+ Array.new(val.map { |e| convert_value(e) })
235
388
  else
236
389
  val
237
390
  end
238
391
  end
392
+
393
+ private
394
+
395
+ def log_built_in_message(method_key)
396
+ return if self.class.disable_warnings?(method_key)
397
+
398
+ method_information = Hashie::Utils.method_information(method(method_key))
399
+
400
+ Hashie.logger.warn(
401
+ 'You are setting a key that conflicts with a built-in method ' \
402
+ "#{self.class}##{method_key} #{method_information}. " \
403
+ 'This can cause unexpected behavior when accessing the key as a ' \
404
+ 'property. You can still access the key via the #[] method.'
405
+ )
406
+ end
407
+
408
+ def log_collision?(method_key)
409
+ return unless respond_to?(method_key)
410
+
411
+ _, suffix = method_name_and_suffix(method_key)
412
+
413
+ (!suffix || suffix == '='.freeze) &&
414
+ !self.class.disable_warnings?(method_key) &&
415
+ !(regular_key?(method_key) || regular_key?(method_key.to_s))
416
+ end
239
417
  end
240
418
  end