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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
3
5
  # SRP: This extension will fail an error whenever a key is accessed
@@ -19,11 +21,21 @@ module Hashie
19
21
  # NOTE: For googlers coming from Python to Ruby, this extension makes a Hash
20
22
  # behave more like a "Dictionary".
21
23
  #
24
+ # @example Raise on missing keys
25
+ # hash = { name: 'Ada' }.extend(Hashie::Extensions::StrictKeyAccess)
26
+ # hash[:name] # => 'Ada'
27
+ # expect { hash[:missing] }.to raise_error(KeyError)
22
28
  module StrictKeyAccess
29
+ # @example Explain why defaults are unsupported
30
+ # error = Hashie::Extensions::StrictKeyAccess::DefaultError.new
31
+ # error.message # => 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense'
23
32
  class DefaultError < StandardError
33
+ # @example Construct a default-access error
34
+ # error = Hashie::Extensions::StrictKeyAccess::DefaultError.new
35
+ # error.message # => 'Setting or using a default with Hashie::Extensions::StrictKeyAccess does not make sense'
24
36
  def initialize
25
- super('Setting or using a default with Hashie::Extensions::StrictKeyAccess'\
26
- ' does not make sense'
37
+ super('Setting or using a default with Hashie::Extensions::StrictKeyAccess ' \
38
+ 'does not make sense'
27
39
  )
28
40
  end
29
41
  end
@@ -45,31 +57,73 @@ module Hashie
45
57
  # >> a["noes"]
46
58
  # KeyError: key not found: "noes"
47
59
  #
60
+ # @example Fetch an existing value without a default fallback
61
+ # hash = { name: 'Ada' }.extend(Hashie::Extensions::StrictKeyAccess)
62
+ # hash[:name] # => 'Ada'
63
+ # expect { hash[:missing] }.to raise_error(KeyError)
48
64
  def [](key)
49
65
  fetch(key)
50
66
  end
51
67
 
68
+ # @example Reject reading the default value
69
+ # hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
70
+ # error_class = begin
71
+ # hash.default
72
+ # rescue Hashie::Extensions::StrictKeyAccess::DefaultError => error
73
+ # error.class
74
+ # end
75
+ # error_class # => Hashie::Extensions::StrictKeyAccess::DefaultError
52
76
  def default(_ = nil)
53
77
  raise DefaultError
54
78
  end
55
79
 
80
+ # @example Reject assigning a default value
81
+ # hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
82
+ # rejected = begin
83
+ # hash.default = 'unknown'
84
+ # false
85
+ # rescue Hashie::Extensions::StrictKeyAccess::DefaultError
86
+ # true
87
+ # end
88
+ # rejected # => true
56
89
  def default=(_)
57
90
  raise DefaultError
58
91
  end
59
92
 
93
+ # @example Reject reading a default proc
94
+ # hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
95
+ # rejected = begin
96
+ # hash.default_proc
97
+ # false
98
+ # rescue Hashie::Extensions::StrictKeyAccess::DefaultError
99
+ # true
100
+ # end
101
+ # rejected # => true
60
102
  def default_proc
61
103
  raise DefaultError
62
104
  end
63
105
 
106
+ # @example Reject assigning a default proc
107
+ # hash = {}.extend(Hashie::Extensions::StrictKeyAccess)
108
+ # rejected = begin
109
+ # hash.default_proc = ->(_hash, _key) { 'unknown' }
110
+ # false
111
+ # rescue Hashie::Extensions::StrictKeyAccess::DefaultError
112
+ # true
113
+ # end
114
+ # rejected # => true
64
115
  def default_proc=(_)
65
116
  raise DefaultError
66
117
  end
67
118
 
119
+ # @example Find an existing key and reject missing values
120
+ # hash = { name: 'Ada', nil => false }.extend(Hashie::Extensions::StrictKeyAccess)
121
+ # hash.key('Ada') # => :name
122
+ # hash.key(false) # => nil
123
+ # expect { hash.key('Grace') }.to raise_error(KeyError)
68
124
  def key(value)
69
125
  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
126
+ raise KeyError, "no key found for value `#{value.inspect}`" if result.nil? && (!key?(result) || self[result] != value)
73
127
  end
74
128
  end
75
129
  end
@@ -1,31 +1,75 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # Extends a Hash with the ability to convert its keys to strings.
6
+ #
7
+ # @example Extend a pre-existing Hash object
8
+ # hash = { test: 'value' }
9
+ # hash.extend(Hashie::Extensions::StringifyKeys)
10
+ # hash.stringify_keys!
11
+ # hash # => { 'test' => 'value' }
12
+ #
13
+ # @example Create a Hash class with this capability
14
+ # class StringifyingHash < Hash
15
+ # include Hashie::Extensions::StringifyKeys
16
+ # end
17
+ #
18
+ # hash = StringifyingHash.new
19
+ # hash[:test] = 'value'
20
+ # hash.stringify_keys!
21
+ # hash # => { 'test' => 'value' }
22
+ #
23
+ # @example Use the module function to stringify keys in a hash
24
+ # hash = { test: 'value' }
25
+ # Hashie::Extensions::StringifyKeys.stringify_keys!(hash)
26
+ # hash # => { 'test' => 'value' }
3
27
  module StringifyKeys
4
- # Convert all keys in the hash to strings.
28
+ # Converts all keys in the hash and nested hashes and arrays to strings.
5
29
  #
6
- # @example
7
- # test = {:abc => 'def'}
30
+ # @example Convert keys in place
31
+ # test = { abc: [{ def: 'value' }] }
32
+ # test.extend(Hashie::Extensions::StringifyKeys)
8
33
  # test.stringify_keys!
9
- # test # => {'abc' => 'def'}
34
+ # test # => { 'abc' => [{ 'def' => 'value' }] }
35
+ #
36
+ # @api public
37
+ # @return [Hash] the receiver with its keys converted to strings
10
38
  def stringify_keys!
11
39
  StringifyKeys.stringify_keys!(self)
12
40
  self
13
41
  end
14
42
 
15
- # Return a new hash with all keys converted
16
- # to strings.
43
+ # Returns a shallow copy with all keys recursively converted to strings.
44
+ # Nested hashes and arrays are shared with the original and are modified.
45
+ #
46
+ # @example Copy a flat hash without modifying the original
47
+ # test = { abc: 'def' }
48
+ # test.extend(Hashie::Extensions::StringifyKeys)
49
+ # test.stringify_keys # => { 'abc' => 'def' }
50
+ # test # => { abc: 'def' }
51
+ #
52
+ # @example Nested values are shared, not deep copied
53
+ # test = { abc: [{ def: 'value' }] }
54
+ # test.extend(Hashie::Extensions::StringifyKeys)
55
+ # test.stringify_keys # => { 'abc' => [{ 'def' => 'value' }] }
56
+ # test # => { abc: [{ 'def' => 'value' }] }
57
+ #
58
+ # @api public
59
+ # @return [Hash] a shallow copy with stringified keys
17
60
  def stringify_keys
18
61
  StringifyKeys.stringify_keys(self)
19
62
  end
20
63
 
64
+ # @example Add recursive conversion functions to an object
65
+ # converter = Object.new.extend(Hashie::Extensions::StringifyKeys::ClassMethods)
66
+ # converter.stringify_keys(user: { name: 'Ada' }) # => { 'user' => { 'name' => 'Ada' } }
21
67
  module ClassMethods
22
68
  # Stringify all keys recursively within nested
23
69
  # hashes and arrays.
24
70
  # @api private
25
71
  def stringify_keys_recursively!(object)
26
72
  case object
27
- when self.class
28
- stringify_keys!(object)
29
73
  when ::Array
30
74
  object.each do |i|
31
75
  stringify_keys_recursively!(i)
@@ -35,13 +79,16 @@ module Hashie
35
79
  end
36
80
  end
37
81
 
38
- # Convert all keys in the hash to strings.
82
+ # Converts all keys in a hash recursively to strings, in place.
83
+ #
84
+ # @example Convert keys through Hashie's module function
85
+ # test = { abc: 'def' }
86
+ # Hashie.stringify_keys!(test)
87
+ # test # => { 'abc' => 'def' }
39
88
  #
40
- # @param [::Hash] hash
41
- # @example
42
- # test = {:abc => 'def'}
43
- # test.stringify_keys!
44
- # test # => {'abc' => 'def'}
89
+ # @api public
90
+ # @param [Hash] hash
91
+ # @return [Hash] the passed hash with stringified keys
45
92
  def stringify_keys!(hash)
46
93
  hash.extend(Hashie::Extensions::StringifyKeys) unless hash.respond_to?(:stringify_keys!)
47
94
  hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
@@ -51,9 +98,17 @@ module Hashie
51
98
  hash
52
99
  end
53
100
 
54
- # Return a copy of hash with all keys converted
55
- # to strings.
56
- # @param [::Hash] hash
101
+ # Returns a shallow copy of a hash with recursively stringified keys.
102
+ # Nested hashes and arrays are shared with the original and are modified.
103
+ #
104
+ # @example Copy keys through Hashie's module function
105
+ # test = { abc: 'def' }
106
+ # Hashie.stringify_keys(test) # => { 'abc' => 'def' }
107
+ # test # => { abc: 'def' }
108
+ #
109
+ # @api public
110
+ # @param [Hash] hash
111
+ # @return [Hash] a shallow copy with stringified keys
57
112
  def stringify_keys(hash)
58
113
  copy = hash.dup
59
114
  copy.extend(Hashie::Extensions::StringifyKeys) unless copy.respond_to?(:stringify_keys!)
@@ -1,31 +1,76 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # Extends a Hash with the ability to convert its keys to symbols.
6
+ # Keys that do not respond to #to_sym are preserved.
7
+ #
8
+ # @example Extend a pre-existing Hash object
9
+ # hash = { 'test' => 'value' }
10
+ # hash.extend(Hashie::Extensions::SymbolizeKeys)
11
+ # hash.symbolize_keys!
12
+ # hash # => { test: 'value' }
13
+ #
14
+ # @example Create a Hash class with this capability
15
+ # class SymbolizableHash < Hash
16
+ # include Hashie::Extensions::SymbolizeKeys
17
+ # end
18
+ #
19
+ # hash = SymbolizableHash.new
20
+ # hash['test'] = 'value'
21
+ # hash.symbolize_keys!
22
+ # hash # => { test: 'value' }
23
+ #
24
+ # @example Use the module function to symbolize keys in a hash
25
+ # hash = { 'test' => 'value', 123 => 'number' }
26
+ # Hashie::Extensions::SymbolizeKeys.symbolize_keys!(hash)
27
+ # hash # => { test: 'value', 123 => 'number' }
3
28
  module SymbolizeKeys
4
- # Convert all keys in the hash to symbols.
29
+ # Converts keys in the hash and nested hashes and arrays to symbols.
5
30
  #
6
- # @example
7
- # test = {'abc' => 'def'}
31
+ # @example Convert keys in place
32
+ # test = { 'abc' => [{ 'def' => 'value' }] }
33
+ # test.extend(Hashie::Extensions::SymbolizeKeys)
8
34
  # test.symbolize_keys!
9
- # test # => {:abc => 'def'}
35
+ # test # => { abc: [{ def: 'value' }] }
36
+ #
37
+ # @api public
38
+ # @return [Hash] the receiver with its keys symbolized where possible
10
39
  def symbolize_keys!
11
40
  SymbolizeKeys.symbolize_keys!(self)
12
41
  self
13
42
  end
14
43
 
15
- # Return a new hash with all keys converted
16
- # to symbols.
44
+ # Returns a shallow copy with keys recursively converted to symbols.
45
+ # Nested hashes and arrays are shared with the original and are modified.
46
+ #
47
+ # @example Copy a flat hash without modifying the original
48
+ # test = { 'abc' => 'def' }
49
+ # test.extend(Hashie::Extensions::SymbolizeKeys)
50
+ # test.symbolize_keys # => { abc: 'def' }
51
+ # test # => { 'abc' => 'def' }
52
+ #
53
+ # @example Nested values are shared, not deep copied
54
+ # test = { 'abc' => [{ 'def' => 'value' }] }
55
+ # test.extend(Hashie::Extensions::SymbolizeKeys)
56
+ # test.symbolize_keys # => { abc: [{ def: 'value' }] }
57
+ # test # => { 'abc' => [{ def: 'value' }] }
58
+ #
59
+ # @api public
60
+ # @return [Hash] a shallow copy with symbolized keys
17
61
  def symbolize_keys
18
62
  SymbolizeKeys.symbolize_keys(self)
19
63
  end
20
64
 
65
+ # @example Add recursive conversion functions to an object
66
+ # converter = Object.new.extend(Hashie::Extensions::SymbolizeKeys::ClassMethods)
67
+ # converter.symbolize_keys('user' => { 'name' => 'Ada' }) # => { user: { name: 'Ada' } }
21
68
  module ClassMethods
22
69
  # Symbolize all keys recursively within nested
23
70
  # hashes and arrays.
24
71
  # @api private
25
72
  def symbolize_keys_recursively!(object)
26
73
  case object
27
- when self.class
28
- symbolize_keys!(object)
29
74
  when ::Array
30
75
  object.each do |i|
31
76
  symbolize_keys_recursively!(i)
@@ -35,13 +80,16 @@ module Hashie
35
80
  end
36
81
  end
37
82
 
38
- # Convert all keys in hash to symbols.
83
+ # Converts keys in a hash recursively to symbols, in place.
84
+ #
85
+ # @example Convert keys through Hashie's module function
86
+ # test = { 'abc' => 'def' }
87
+ # Hashie.symbolize_keys!(test)
88
+ # test # => { abc: 'def' }
39
89
  #
90
+ # @api public
40
91
  # @param [Hash] hash
41
- # @example
42
- # test = {'abc' => 'def'}
43
- # Hashie.symbolize_keys! test
44
- # test # => {:abc => 'def'}
92
+ # @return [Hash] the passed hash with symbolized keys
45
93
  def symbolize_keys!(hash)
46
94
  hash.extend(Hashie::Extensions::SymbolizeKeys) unless hash.respond_to?(:symbolize_keys!)
47
95
  hash.keys.each do |k| # rubocop:disable Performance/HashEachMethods
@@ -51,9 +99,17 @@ module Hashie
51
99
  hash
52
100
  end
53
101
 
54
- # Return a copy of hash with all keys converted
55
- # to symbols.
56
- # @param [::Hash] hash
102
+ # Returns a shallow copy of a hash with recursively symbolized keys.
103
+ # Nested hashes and arrays are shared with the original and are modified.
104
+ #
105
+ # @example Copy keys through Hashie's module function
106
+ # test = { 'abc' => 'def' }
107
+ # Hashie.symbolize_keys(test) # => { abc: 'def' }
108
+ # test # => { 'abc' => 'def' }
109
+ #
110
+ # @api public
111
+ # @param [Hash] hash
112
+ # @return [Hash] a shallow copy with symbolized keys
57
113
  def symbolize_keys(hash)
58
114
  copy = hash.dup
59
115
  copy.extend(Hashie::Extensions::SymbolizeKeys) unless copy.respond_to?(:symbolize_keys!)
data/lib/hashie/hash.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/extensions/stringify_keys'
2
4
  require 'hashie/extensions/pretty_inspect'
3
5
 
@@ -5,16 +7,26 @@ module Hashie
5
7
  # A Hashie Hash is simply a Hash that has convenience
6
8
  # functions baked in such as stringify_keys that may
7
9
  # not be available in all libraries.
10
+ #
11
+ # @example Convert keys using the built-in extensions
12
+ # Hashie::Hash[name: 'Ada'].stringify_keys # => { 'name' => 'Ada' }
8
13
  class Hash < ::Hash
9
14
  include Hashie::Extensions::PrettyInspect
10
15
  include Hashie::Extensions::StringifyKeys
11
16
 
12
17
  # Convert this hash into a Mash
18
+ #
19
+ # @example Read converted data through methods
20
+ # Hashie::Hash[name: 'Ada'].to_mash.name # => 'Ada'
13
21
  def to_mash
14
22
  ::Hashie::Mash.new(self)
15
23
  end
16
24
 
17
25
  # Converts a mash back to a hash (with stringified or symbolized keys)
26
+ #
27
+ # @example Recursively convert nested hashes and their keys
28
+ # hash = Hashie::Hash[user: Hashie::Hash[name: 'Ada']]
29
+ # hash.to_hash(stringify_keys: true) # => { 'user' => { 'name' => 'Ada' } }
18
30
  def to_hash(options = {})
19
31
  out = {}
20
32
  each_key do |k|
@@ -39,6 +51,10 @@ module Hashie
39
51
  end
40
52
 
41
53
  # The C generator for the json gem doesn't like mashies
54
+ #
55
+ # @example Serialize as an ordinary JSON object
56
+ # require 'json'
57
+ # JSON.parse(Hashie::Hash[name: 'Ada'].to_json) # => { 'name' => 'Ada' }
42
58
  def to_json(*args)
43
59
  to_hash.to_json(*args)
44
60
  end
data/lib/hashie/logger.rb CHANGED
@@ -1,17 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  module Hashie
4
6
  # The logger that Hashie uses for reporting errors.
5
7
  #
6
8
  # @return [Logger]
9
+ #
10
+ # @example Obtain the logger used for warnings
11
+ # Hashie.logger.respond_to?(:warn) # => true
7
12
  def self.logger
8
- @logger ||= Logger.new(STDOUT)
13
+ @logger ||= Logger.new($stdout)
9
14
  end
10
15
 
11
16
  # Sets the logger that Hashie uses for reporting errors.
12
17
  #
13
18
  # @param logger [Logger] The logger to set as Hashie's logger.
14
19
  # @return [void]
20
+ #
21
+ # @example Temporarily capture warnings
22
+ # require 'stringio'
23
+ # original = Hashie.logger
24
+ # begin
25
+ # output = StringIO.new
26
+ # Hashie.logger = Logger.new(output)
27
+ # Hashie.logger.warn('example warning')
28
+ # output.string.include?('example warning') # => true
29
+ # ensure
30
+ # Hashie.logger = original
31
+ # end
15
32
  def self.logger=(logger)
16
33
  @logger = logger
17
34
  end