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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -0
- data/CONTRIBUTING.md +38 -1
- data/README.md +816 -181
- data/Rakefile +29 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +8 -7
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +144 -16
- 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 -19
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
- data/lib/hashie/extensions/dash/property_translation.rb +134 -16
- 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 +15 -28
- 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 +21 -3
- data/lib/hashie/extensions/indifferent_access.rb +100 -14
- 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 +6 -2
- 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 +93 -7
- 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 +72 -16
- data/lib/hashie/hash.rb +16 -0
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +293 -62
- 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 +45 -0
- 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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
55
|
-
# #
|
|
56
|
-
#
|
|
57
|
-
# books.deep_locate
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
'
|
|
8
|
-
'
|
|
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 [
|
|
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
|
-
@
|
|
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
|