hashie 2.1.2 → 4.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.
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