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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -53
- data/CONTRIBUTING.md +38 -1
- data/LICENSE +1 -1
- data/README.md +920 -205
- data/Rakefile +32 -1
- data/UPGRADING.md +56 -0
- data/hashie.gemspec +10 -5
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +160 -32
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -26
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
- data/lib/hashie/extensions/dash/property_translation.rb +138 -15
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +16 -29
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
- data/lib/hashie/extensions/indifferent_access.rb +124 -5
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +94 -8
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +84 -17
- data/lib/hashie/hash.rb +18 -2
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +314 -82
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +46 -0
- 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 =
|
|
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
|
-
'
|
|
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
|
-
#
|
|
28
|
+
# Converts all keys in the hash and nested hashes and arrays to strings.
|
|
5
29
|
#
|
|
6
|
-
# @example
|
|
7
|
-
# test = {:
|
|
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
|
-
#
|
|
16
|
-
#
|
|
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
|
-
#
|
|
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
|
-
# @
|
|
41
|
-
# @
|
|
42
|
-
#
|
|
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
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
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!)
|