hashie 4.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. metadata +22 -9
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'pathname'
5
+
6
+ module Hashie
7
+ module Extensions
8
+ module Parsers
9
+ # Parses YAML without evaluating ERB templates.
10
+ #
11
+ # @example
12
+ # require 'tempfile'
13
+ # file = Tempfile.new(['hashie-example', '.yml'], '.')
14
+ # begin
15
+ # file.write("name: Ada\n")
16
+ # file.close
17
+ # Hashie::Extensions::Parsers::YamlParser.perform(file.path) # => {'name' => 'Ada'}
18
+ # ensure
19
+ # file.close!
20
+ # end
21
+ class YamlParser
22
+ # @example Read a Pathname and retain parsing options
23
+ # require 'tempfile'
24
+ # file = Tempfile.new(['hashie-example', '.yml'], '.')
25
+ # begin
26
+ # file.write("name: Ada\n")
27
+ # file.close
28
+ # parser = Hashie::Extensions::Parsers::YamlParser.new(Pathname.new(file.path), aliases: false)
29
+ # parser.perform # => {'name' => 'Ada'}
30
+ # ensure
31
+ # file.close!
32
+ # end
33
+ def initialize(file_path, options = {})
34
+ @content = File.read(file_path)
35
+ @file_path = file_path.is_a?(Pathname) ? file_path.to_s : file_path
36
+ @options = options
37
+ end
38
+
39
+ # @example Leave ERB untouched when loading YAML
40
+ # require 'tempfile'
41
+ # file = Tempfile.new(['hashie-example', '.yml'], '.')
42
+ # begin
43
+ # file.write("value: <%= 1 + 2 %>\n")
44
+ # file.close
45
+ # parser = Hashie::Extensions::Parsers::YamlParser.new(file.path)
46
+ # parser.perform # => {'value' => '<%= 1 + 2 %>'}
47
+ # ensure
48
+ # file.close!
49
+ # end
50
+ def perform
51
+ yaml_safe_load(@content)
52
+ end
53
+
54
+ # @example Load YAML in one step
55
+ # require 'tempfile'
56
+ # file = Tempfile.new(['hashie-example', '.yml'], '.')
57
+ # begin
58
+ # file.write("count: 3\n")
59
+ # file.close
60
+ # Hashie::Extensions::Parsers::YamlParser.perform(file.path, aliases: false) # => {'count' => 3}
61
+ # ensure
62
+ # file.close!
63
+ # end
64
+ def self.perform(file_path, options = {})
65
+ new(file_path, options).perform
66
+ end
67
+
68
+ private
69
+
70
+ def yaml_safe_load(content)
71
+ YAML.safe_load(
72
+ content,
73
+ permitted_classes: @options.fetch(:permitted_classes) { [] },
74
+ permitted_symbols: @options.fetch(:permitted_symbols) { [] },
75
+ aliases: @options.fetch(:aliases, true)
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+ require 'tempfile'
5
+
6
+ module Hashie
7
+ module Extensions
8
+ # Opt-in YAML file persistence for classes initialized from a hash.
9
+ #
10
+ # @example Save and reload a hash subclass
11
+ # class Settings < Hash
12
+ # include Hashie::Extensions::MergeInitializer
13
+ # include Hashie::Extensions::Persistable
14
+ # end
15
+ # file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
16
+ # begin
17
+ # settings = Settings.new('theme' => 'dark')
18
+ # settings.save(file.path)
19
+ # Settings.load(file.path)['theme'] # => 'dark'
20
+ # ensure
21
+ # file.close!
22
+ # end
23
+ module Persistable
24
+ def self.included(base)
25
+ base.extend ClassMethods
26
+ end
27
+
28
+ # @example Add a YAML loader by including Persistable
29
+ # class Settings < Hash
30
+ # include Hashie::Extensions::MergeInitializer
31
+ # include Hashie::Extensions::Persistable
32
+ # end
33
+ # file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
34
+ # begin
35
+ # file.write("---\ntheme: dark\n")
36
+ # file.flush
37
+ # Settings.load(file.path) # => { 'theme' => 'dark' }
38
+ # ensure
39
+ # file.close!
40
+ # end
41
+ module ClassMethods
42
+ # @example Load safe YAML data and remember its path
43
+ # class Settings < Hash
44
+ # include Hashie::Extensions::MergeInitializer
45
+ # include Hashie::Extensions::Persistable
46
+ # end
47
+ # file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
48
+ # begin
49
+ # file.write("---\ntheme: dark\n")
50
+ # file.flush
51
+ # settings = Settings.load(file.path)
52
+ # settings['theme'] # => 'dark'
53
+ # settings.persistable_path # => File.expand_path(file.path)
54
+ # ensure
55
+ # file.close!
56
+ # end
57
+ def load(path)
58
+ path = File.expand_path(File.path(path))
59
+ data = Psych.safe_load(File.read(path), permitted_classes: [Symbol], aliases: false)
60
+ raise TypeError, 'persisted YAML must contain a hash' unless data.is_a?(::Hash)
61
+
62
+ new(data).tap { |object| object.instance_variable_set(:@persistable_path, path) }
63
+ end
64
+ end
65
+
66
+ # @example Remember the last successful save location
67
+ # class Settings < Hash
68
+ # include Hashie::Extensions::MergeInitializer
69
+ # include Hashie::Extensions::Persistable
70
+ # end
71
+ # file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
72
+ # begin
73
+ # settings = Settings.new('theme' => 'dark')
74
+ # settings.persistable_path # => nil
75
+ # settings.save(file.path)
76
+ # settings.persistable_path # => File.expand_path(file.path)
77
+ # ensure
78
+ # file.close!
79
+ # end
80
+ attr_reader :persistable_path
81
+
82
+ # @example Save again without repeating the path
83
+ # class Settings < Hash
84
+ # include Hashie::Extensions::MergeInitializer
85
+ # include Hashie::Extensions::Persistable
86
+ # end
87
+ # file = Tempfile.new(['hashie-example', '.yaml'], Dir.pwd)
88
+ # begin
89
+ # settings = Settings.new('theme' => 'dark')
90
+ # settings.save(file.path)
91
+ # settings['theme'] = 'light'
92
+ # settings.save
93
+ # Settings.load(file.path)['theme'] # => 'light'
94
+ # ensure
95
+ # file.close!
96
+ # end
97
+ def save(path = persistable_path)
98
+ raise ArgumentError, 'a persistence path is required' if path.nil?
99
+
100
+ path = File.expand_path(File.path(path))
101
+ yaml = Psych.dump(persistable_data(self))
102
+ Tempfile.create(['.hashie-', '.yaml'], File.dirname(path)) do |file|
103
+ file.chmod(File.stat(path).mode & 0o777) if File.exist?(path)
104
+ file.write(yaml)
105
+ file.flush
106
+ file.fsync
107
+ file.close
108
+ File.rename(file.path, path)
109
+ end
110
+ @persistable_path = path
111
+ end
112
+
113
+ private
114
+
115
+ def persistable_data(value, ancestors = [])
116
+ raise ArgumentError, 'cannot persist cyclic data' if ancestors.any? { |ancestor| ancestor.equal?(value) }
117
+
118
+ case value
119
+ when ::Hash
120
+ value.each_with_object({}) do |(key, child), data|
121
+ raise TypeError, 'persisted hash keys must be strings or symbols' unless key.is_a?(String) || key.is_a?(Symbol)
122
+
123
+ data[persistable_data(key)] = persistable_data(child, ancestors + [value])
124
+ end
125
+ when ::Array
126
+ value.map { |child| persistable_data(child, ancestors + [value]) }
127
+ when String
128
+ value.dup
129
+ when Symbol, Integer, Float, TrueClass, FalseClass, NilClass
130
+ value
131
+ else
132
+ raise TypeError, "cannot persist a value of class `#{value.class}`"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -1,11 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # @example Inspect sorted hash entries
6
+ # class Settings < Hash
7
+ # include Hashie::Extensions::PrettyInspect
8
+ # end
9
+ # hash = Settings[:z, 2, :a, 1]
10
+ # hash.inspect # => "#<#{Settings} a=1 z=2>"
3
11
  module PrettyInspect
4
12
  def self.included(base)
5
13
  base.send :alias_method, :hash_inspect, :inspect
6
14
  base.send :alias_method, :inspect, :hashie_inspect
7
15
  end
8
16
 
17
+ # @example Include the class and sorted key/value pairs
18
+ # class Settings < Hash
19
+ # include Hashie::Extensions::PrettyInspect
20
+ # end
21
+ # hash = Settings[:z, 2, :a, 1]
22
+ # hash.hashie_inspect # => "#<#{Settings} a=1 z=2>"
9
23
  def hashie_inspect
10
24
  ret = "#<#{self.class}"
11
25
  keys.sort_by(&:to_s).each do |key|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (c) Chad Fowler, Rich Kilmer, Jim Weirich and others.
2
4
  # Portions copyright (c) Engine Yard and Andre Arko
3
5
  #
@@ -14,22 +16,37 @@
14
16
 
15
17
  module Hashie
16
18
  module Extensions
19
+ # @example Compare versions numerically
20
+ # Hashie::Extensions::RubyVersion.new('3.10') > Hashie::Extensions::RubyVersion.new('3.9') # => true
17
21
  class RubyVersion
18
22
  include Comparable
19
23
 
24
+ # @example Inspect and replace parsed version segments
25
+ # version = Hashie::Extensions::RubyVersion.new('3.2.0.preview1')
26
+ # version.segments # => [3, 2, 0, 'preview', 1]
27
+ # version.segments = [3, 3, 0]
28
+ # version == Hashie::Extensions::RubyVersion.new('3.3') # => true
20
29
  attr_accessor :segments
21
30
 
31
+ # @example Parse numeric and prerelease segments
32
+ # version = Hashie::Extensions::RubyVersion.new('3.2.0.rc1')
33
+ # version.segments # => [3, 2, 0, 'rc', 1]
22
34
  def initialize(version)
23
35
  @segments = split_to_segments(version)
24
36
  end
25
37
 
38
+ # @example Ignore trailing zeros and order prereleases before releases
39
+ # version = Hashie::Extensions::RubyVersion
40
+ # version.new('3.2') <=> version.new('3.2.0') # => 0
41
+ # version.new('3.2.rc1') <=> version.new('3.2') # => -1
42
+ # version.new('3.10') <=> version.new('3.9') # => 1
26
43
  def <=>(other)
27
44
  lhsegments = segments
28
45
  rhsegments = other.segments
29
46
 
30
47
  lhsize = lhsegments.size
31
48
  rhsize = rhsegments.size
32
- limit = (lhsize > rhsize ? lhsize : rhsize) - 1
49
+ limit = [lhsize, rhsize].max - 1
33
50
 
34
51
  i = 0
35
52
 
@@ -1,17 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'hashie/extensions/ruby_version'
2
4
 
3
5
  module Hashie
4
6
  module Extensions
7
+ # @example Conditionally run code for the current Ruby
8
+ # klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
9
+ # klass.with_minimum_ruby(RUBY_VERSION) { 'supported' } # => 'supported'
5
10
  module RubyVersionCheck
6
11
  def self.included(base)
7
12
  base.extend ClassMethods
8
13
  end
9
14
 
15
+ # @example Add version checks to a class
16
+ # klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
17
+ # klass.with_minimum_ruby?('0.0.0') # => true
18
+ # klass.with_minimum_ruby?('999.0.0') # => false
10
19
  module ClassMethods
20
+ # @example Run the block only when the minimum is satisfied
21
+ # klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
22
+ # klass.with_minimum_ruby('0.0.0') { 'supported' } # => 'supported'
23
+ # klass.with_minimum_ruby('999.0.0') { raise 'not reached' } # => nil
11
24
  def with_minimum_ruby(version)
12
25
  yield if with_minimum_ruby?(version)
13
26
  end
14
27
 
28
+ # @example Accept the current version as a minimum
29
+ # klass = Class.new { include Hashie::Extensions::RubyVersionCheck }
30
+ # klass.with_minimum_ruby?(RUBY_VERSION) # => true
31
+ # klass.with_minimum_ruby?('999.0.0') # => false
15
32
  def with_minimum_ruby?(version)
16
33
  RubyVersion.new(RUBY_VERSION) >= RubyVersion.new(version)
17
34
  end
@@ -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!)