hashie 5.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CONTRIBUTING.md +38 -1
  4. data/README.md +816 -181
  5. data/Rakefile +29 -1
  6. data/UPGRADING.md +18 -0
  7. data/hashie.gemspec +8 -7
  8. data/lib/hashie/array.rb +15 -12
  9. data/lib/hashie/clash.rb +59 -4
  10. data/lib/hashie/dash.rb +144 -16
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  12. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  13. data/lib/hashie/extensions/block_access.rb +71 -0
  14. data/lib/hashie/extensions/coercion.rb +180 -19
  15. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
  17. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  18. data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
  19. data/lib/hashie/extensions/dash/property_translation.rb +134 -16
  20. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  21. data/lib/hashie/extensions/deep_find.rb +20 -0
  22. data/lib/hashie/extensions/deep_grep.rb +69 -0
  23. data/lib/hashie/extensions/deep_locate.rb +15 -28
  24. data/lib/hashie/extensions/deep_merge.rb +16 -0
  25. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  26. data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
  27. data/lib/hashie/extensions/indifferent_access.rb +100 -14
  28. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  29. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  30. data/lib/hashie/extensions/key_conversion.rb +9 -0
  31. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  32. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  33. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  34. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  35. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  36. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  37. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
  38. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  39. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  40. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  41. data/lib/hashie/extensions/method_access.rb +93 -7
  42. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  43. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  44. data/lib/hashie/extensions/persistable.rb +137 -0
  45. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  46. data/lib/hashie/extensions/ruby_version.rb +18 -1
  47. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  48. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  49. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  50. data/lib/hashie/extensions/symbolize_keys.rb +72 -16
  51. data/lib/hashie/hash.rb +16 -0
  52. data/lib/hashie/logger.rb +18 -1
  53. data/lib/hashie/mash.rb +293 -62
  54. data/lib/hashie/railtie.rb +27 -1
  55. data/lib/hashie/rash.rb +64 -4
  56. data/lib/hashie/trash.rb +8 -0
  57. data/lib/hashie/utils.rb +17 -6
  58. data/lib/hashie/version.rb +3 -1
  59. data/lib/hashie.rb +45 -0
  60. metadata +15 -17
@@ -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
8
  # The module level implementation of #deep_locate, in case you do not want
5
- # to include/extend the base datastructure. For further examples please
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,17 +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
34
44
 
35
45
  klass = self.class
36
- translations = klass.respond_to?(:translations) && klass.translations || []
46
+ translations = (klass.respond_to?(:translations) && klass.translations) || []
37
47
 
38
48
  super(attributes.select { |attr, _| klass.property?(attr) || translations.include?(attr) })
39
49
  end
40
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
41
59
  def property_exists?(property)
42
60
  self.class.property?(property)
43
61
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
3
5
  # IndifferentAccess gives you the ability to not care
@@ -23,10 +25,10 @@ module Hashie
23
25
  # h['baz'] # => 'blip'
24
26
  #
25
27
  module IndifferentAccess
26
- include Hashie::Extensions::RubyVersionCheck
27
-
28
28
  # @api private
29
29
  def self.convert_key(key)
30
+ return key.name if key.is_a?(Symbol)
31
+
30
32
  key.to_s
31
33
  end
32
34
 
@@ -61,6 +63,12 @@ module Hashie
61
63
  # This will inject indifferent access into an instance of
62
64
  # a hash without modifying the actual class. This is what
63
65
  # allows IndifferentAccess to spread to sub-hashes.
66
+ #
67
+ # @example Convert an existing hash in place
68
+ # hash = { user: { name: 'Ada' } }
69
+ # Hashie::Extensions::IndifferentAccess.inject!(hash)
70
+ # hash['user'][:name] # => 'Ada'
71
+ # hash.class # => Hash
64
72
  def self.inject!(hash)
65
73
  (class << hash; self; end).send :include, IndifferentAccess
66
74
  hash.convert!
@@ -68,10 +76,19 @@ module Hashie
68
76
 
69
77
  # Injects indifferent access into a duplicate of the hash
70
78
  # provided. See #inject!
79
+ #
80
+ # @example Leave the original hash's top-level keys unchanged
81
+ # original = { name: 'Ada' }
82
+ # hash = Hashie::Extensions::IndifferentAccess.inject(original)
83
+ # hash['name'] # => 'Ada'
84
+ # original # => { name: 'Ada' }
71
85
  def self.inject(hash)
72
86
  inject!(hash.dup)
73
87
  end
74
88
 
89
+ # @example Normalize symbol keys
90
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
91
+ # hash.convert_key(:name) # => 'name'
75
92
  def convert_key(key)
76
93
  IndifferentAccess.convert_key(key)
77
94
  end
@@ -79,6 +96,13 @@ module Hashie
79
96
  # Iterates through the keys and values, reconverting them to
80
97
  # their proper indifferent state. Used when IndifferentAccess
81
98
  # is injecting itself into member hashes.
99
+ #
100
+ # @example Normalize keys inserted through the original writer
101
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
102
+ # hash.regular_writer(:name, 'Ada')
103
+ # hash.convert!
104
+ # hash.keys # => ['name']
105
+ # hash[:name] # => 'Ada'
82
106
  def convert!
83
107
  keys.each do |k| # rubocop:disable Performance/HashEachMethods
84
108
  indifferent_writer k, regular_delete(k)
@@ -86,6 +110,10 @@ module Hashie
86
110
  self
87
111
  end
88
112
 
113
+ # @example Make hashes nested in arrays indifferent
114
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
115
+ # value = hash.indifferent_value([{ name: 'Ada' }])
116
+ # value.first['name'] # => 'Ada'
89
117
  def indifferent_value(value)
90
118
  if hash_lacking_indifference?(value)
91
119
  IndifferentAccess.inject!(value)
@@ -96,58 +124,114 @@ module Hashie
96
124
  end
97
125
  end
98
126
 
127
+ # @example Return an existing value or the configured default
128
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
129
+ # hash.default = 'unknown'
130
+ # hash.indifferent_default(:name) # => 'Ada'
131
+ # hash.indifferent_default(:missing) # => 'unknown'
99
132
  def indifferent_default(key = nil)
100
133
  return self[convert_key(key)] if key?(key)
134
+
101
135
  regular_default(key)
102
136
  end
103
137
 
138
+ # @example Normalize incoming keys and nested values
139
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
140
+ # hash.indifferent_update(user: { name: 'Ada' })
141
+ # hash['user']['name'] # => 'Ada'
104
142
  def indifferent_update(other_hash)
105
143
  return regular_update(other_hash) if hash_with_indifference?(other_hash)
144
+
106
145
  other_hash.each_pair do |k, v|
107
146
  self[k] = v
108
147
  end
109
148
  end
110
149
 
150
+ # @example Store a symbol key as a string
151
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
152
+ # hash.indifferent_writer(:name, 'Ada')
153
+ # hash # => { 'name' => 'Ada' }
111
154
  def indifferent_writer(key, value)
112
155
  regular_writer convert_key(key), indifferent_value(value)
113
156
  end
114
157
 
115
- def indifferent_fetch(key, *args, &block)
116
- regular_fetch convert_key(key), *args, &block
158
+ # @example Fetch with either key type and support fallbacks
159
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
160
+ # hash.indifferent_fetch(:name) # => 'Ada'
161
+ # hash.indifferent_fetch(:missing, 'unknown') # => 'unknown'
162
+ def indifferent_fetch(key, ...)
163
+ regular_fetch(convert_key(key), ...)
117
164
  end
118
165
 
166
+ # @example Delete a string key through a symbol
167
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
168
+ # hash.indifferent_delete(:name) # => 'Ada'
169
+ # hash # => {}
119
170
  def indifferent_delete(key)
120
171
  regular_delete convert_key(key)
121
172
  end
122
173
 
174
+ # @example Recognize both key representations
175
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
176
+ # hash.indifferent_key?(:name) # => true
177
+ # hash.indifferent_key?('name') # => true
123
178
  def indifferent_key?(key)
124
179
  regular_key? convert_key(key)
125
180
  end
126
181
 
182
+ # @example Retrieve values using mixed key types
183
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
184
+ # hash.indifferent_values_at(:name, 'age', :missing) # => ['Ada', 42, nil]
127
185
  def indifferent_values_at(*indices)
128
186
  indices.map { |i| self[i] }
129
187
  end
130
188
 
189
+ # @example Identify an indifferent hash
190
+ # hash = Hashie::Extensions::IndifferentAccess.inject({})
191
+ # hash.indifferent_access? # => true
131
192
  def indifferent_access?
132
193
  true
133
194
  end
134
195
 
196
+ # @example Replace existing data and convert incoming keys
197
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ old: true })
198
+ # hash.indifferent_replace(name: 'Ada')
199
+ # hash # => { 'name' => 'Ada' }
135
200
  def indifferent_replace(other_hash)
136
201
  (keys - other_hash.keys).each { |key| delete(key) }
137
202
  other_hash.each { |key, value| self[key] = value }
138
203
  self
139
204
  end
140
205
 
206
+ # @example Merge without changing the original
207
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
208
+ # merged = hash.merge(age: 42)
209
+ # merged[:age] # => 42
210
+ # merged.indifferent_access? # => true
211
+ # hash.key?(:age) # => false
141
212
  def merge(*args)
142
213
  result = super
143
214
  return IndifferentAccess.inject!(result) if hash_lacking_indifference?(result)
215
+
144
216
  result.convert!
145
217
  end
146
218
 
219
+ # @example Merge and normalize in place
220
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
221
+ # hash.merge!(age: 42)
222
+ # hash['age'] # => 42
223
+ # hash.keys.sort # => ['age', 'name']
147
224
  def merge!(*)
148
225
  super.convert!
149
226
  end
150
227
 
228
+ # @example Export a regular hash, preserving its default
229
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada' })
230
+ # hash.default = 'unknown'
231
+ # plain = hash.to_hash
232
+ # plain # => { 'name' => 'Ada' }
233
+ # plain[:name] # => 'unknown'
234
+ # plain.respond_to?(:indifferent_access?) # => false
151
235
  def to_hash
152
236
  {}.tap do |result|
153
237
  each_pair { |key, value| result[key] = value }
@@ -160,18 +244,20 @@ module Hashie
160
244
  end
161
245
  end
162
246
 
163
- with_minimum_ruby('2.5.0') do
164
- def slice(*keys)
165
- string_keys = keys.map { |key| convert_key(key) }
166
- super(*string_keys)
167
- end
247
+ # @example Select keys using either representation
248
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
249
+ # hash.slice(:name) # => { 'name' => 'Ada' }
250
+ def slice(*keys)
251
+ string_keys = keys.map { |key| convert_key(key) }
252
+ super(*string_keys)
168
253
  end
169
254
 
170
- with_minimum_ruby('3.0.0') do
171
- def except(*keys)
172
- string_keys = keys.map { |key| convert_key(key) }
173
- super(*string_keys)
174
- end
255
+ # @example Exclude keys using either representation
256
+ # hash = Hashie::Extensions::IndifferentAccess.inject({ name: 'Ada', age: 42 })
257
+ # hash.except(:age) # => { 'name' => 'Ada' }
258
+ def except(*keys)
259
+ string_keys = keys.map { |key| convert_key(key) }
260
+ super(*string_keys)
175
261
  end
176
262
 
177
263
  protected
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hashie
4
+ module Extensions
5
+ # @example Opt in to strict numeric coercion
6
+ # klass = Class.new(Hash) do
7
+ # include Hashie::Extensions::Coercion
8
+ # extend Hashie::Extensions::KernelCoercion
9
+ # coerce_key :age, Integer
10
+ # end
11
+ # hash = klass.new
12
+ # hash[:age] = '42'
13
+ # hash[:age] # => 42
14
+ # expect { hash[:age] = 'bad' }.to raise_error(Hashie::CoercionError)
15
+ module KernelCoercion
16
+ # Core types whose values can be coerced with a stricter
17
+ # +Kernel+ method (e.g. +Kernel#Integer+) instead of the more
18
+ # lenient +#to_i+-style conversion methods used by default.
19
+ #
20
+ # NOTE: There is no strict Kernel method equivalent for +Symbol+, so
21
+ # +Symbol+ coercion is left to the default, lenient +#to_sym+ behavior.
22
+ STRICT_CORE_TYPES = {
23
+ Integer => :Integer,
24
+ Float => :Float,
25
+ Complex => :Complex,
26
+ Rational => :Rational,
27
+ String => :String
28
+ }.freeze
29
+
30
+ # Overrides Hashie::Extensions::Coercion::ClassMethods#build_core_type_coercion
31
+ # to use a stricter Kernel method (e.g. Kernel#Integer) for coercing core
32
+ # types, instead of the default, more lenient conversion method (e.g. #to_i).
33
+ #
34
+ # "abcd".to_i #=> 0
35
+ # Integer("abcd") #=> ArgumentError: invalid value for Integer(): "abcd"
36
+ #
37
+ # @example Build a strict conversion and preserve Symbol fallback
38
+ # klass = Class.new(Hash) do
39
+ # include Hashie::Extensions::Coercion
40
+ # extend Hashie::Extensions::KernelCoercion
41
+ # end
42
+ # conversion = klass.build_core_type_coercion(Integer)
43
+ # conversion.call('42') # => 42
44
+ # expect { conversion.call('42 items') }.to raise_error(ArgumentError)
45
+ # klass.build_core_type_coercion(Symbol).call('name') # => :name
46
+ def build_core_type_coercion(type)
47
+ return super unless STRICT_CORE_TYPES.key?(type)
48
+
49
+ kernel_method = STRICT_CORE_TYPES[type]
50
+ lambda do |value|
51
+ next value if value.is_a?(type)
52
+
53
+ Kernel.public_send(kernel_method, value)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,11 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # @example Disable conflict warnings on a Mash subclass
6
+ # klass = Class.new(Hashie::Mash)
7
+ # klass.disable_warnings(:size)
8
+ # klass.disable_warnings?(:size) # => true
9
+ # klass.disable_warnings?(:keys) # => false
3
10
  module KeyConflictWarning
11
+ # @example Explain why base Mash warnings cannot be disabled
12
+ # require 'hashie/mash'
13
+ # error = Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings.new
14
+ # error.message.include?('please subclass the Mash') # => true
4
15
  class CannotDisableMashWarnings < StandardError
16
+ # @example Construct the warning configuration error
17
+ # require 'hashie/mash'
18
+ # error = Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings.new
19
+ # error.message # => 'you cannot disable warnings on the base Mash class; please subclass the Mash and disable it in the subclass'
5
20
  def initialize
6
21
  super(
7
- 'You cannot disable warnings on the base Mash class. ' \
8
- 'Please subclass the Mash and disable it in the subclass.'
22
+ 'you cannot disable warnings on the base Mash class; ' \
23
+ 'please subclass the Mash and disable it in the subclass'
9
24
  )
10
25
  end
11
26
  end
@@ -14,8 +29,13 @@ module Hashie
14
29
  #
15
30
  # @api semipublic
16
31
  # @return [void]
32
+ # @example Disable warnings for selected methods
33
+ # klass = Class.new(Hashie::Mash)
34
+ # klass.disable_warnings(:size, :keys)
35
+ # klass.disabled_warnings # => [:size, :keys]
17
36
  def disable_warnings(*method_keys)
18
37
  raise CannotDisableMashWarnings if self == Hashie::Mash
38
+
19
39
  if method_keys.any?
20
40
  disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
21
41
  else
@@ -29,23 +49,38 @@ module Hashie
29
49
  #
30
50
  # @api semipublic
31
51
  # @return [Boolean]
52
+ # @example Disable all warnings when no methods are specified
53
+ # klass = Class.new(Hashie::Mash)
54
+ # klass.disable_warnings
55
+ # klass.disable_warnings?(:size) # => true
32
56
  def disable_warnings?(method_key = nil)
33
57
  return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
58
+
34
59
  @disable_warnings ||= false
35
60
  end
36
61
 
37
62
  # Returns an array of methods that this class disables warnings for.
38
63
  #
39
64
  # @api semipublic
40
- # @return [Boolean]
65
+ # @return [Array<Symbol>]
66
+ # @example Inspect selected warning exclusions
67
+ # klass = Class.new(Hashie::Mash)
68
+ # klass.disable_warnings(:size)
69
+ # klass.disabled_warnings # => [:size]
41
70
  def disabled_warnings
42
- @_disabled_warnings ||= []
71
+ @disabled_warnings ||= []
43
72
  end
44
73
 
45
74
  # Inheritance hook that sets class configuration when inherited.
46
75
  #
47
76
  # @api semipublic
48
77
  # @return [void]
78
+ # @example Inherit selected warning settings
79
+ # parent = Class.new(Hashie::Mash)
80
+ # parent.disable_warnings(:size)
81
+ # child = Class.new(parent)
82
+ # child.disabled_warnings # => [:size]
83
+ # child.disable_warnings?(:keys) # => false
49
84
  def inherited(subclass)
50
85
  super
51
86
  subclass.disable_warnings(disabled_warnings) if disable_warnings?
@@ -1,5 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # @example Include both key conversion extensions
6
+ # class ConvertibleHash < Hash
7
+ # include Hashie::Extensions::KeyConversion
8
+ # end
9
+ # hash = ConvertibleHash[:name, 'Ada']
10
+ # hash.stringify_keys # => { 'name' => 'Ada' }
11
+ # hash.stringify_keys!.symbolize_keys # => { name: 'Ada' }
3
12
  module KeyConversion
4
13
  def self.included(base)
5
14
  base.send :include, SymbolizeKeys