hashie 3.4.3 → 5.0.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 +5 -5
- data/CHANGELOG.md +516 -129
- data/CONTRIBUTING.md +24 -7
- data/LICENSE +1 -1
- data/README.md +408 -50
- data/Rakefile +18 -1
- data/UPGRADING.md +157 -7
- data/hashie.gemspec +14 -8
- data/lib/hashie/array.rb +21 -0
- data/lib/hashie/clash.rb +24 -12
- data/lib/hashie/dash.rb +56 -31
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
- data/lib/hashie/extensions/coercion.rb +30 -17
- data/lib/hashie/extensions/dash/indifferent_access.rb +30 -1
- data/lib/hashie/extensions/dash/predefined_values.rb +88 -0
- data/lib/hashie/extensions/dash/property_translation.rb +59 -28
- data/lib/hashie/extensions/deep_fetch.rb +5 -3
- data/lib/hashie/extensions/deep_find.rb +14 -5
- data/lib/hashie/extensions/deep_locate.rb +40 -21
- data/lib/hashie/extensions/deep_merge.rb +26 -10
- data/lib/hashie/extensions/ignore_undeclared.rb +6 -4
- data/lib/hashie/extensions/indifferent_access.rb +49 -8
- data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
- data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
- data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
- data/lib/hashie/extensions/method_access.rb +77 -19
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +29 -5
- data/lib/hashie/extensions/ruby_version.rb +60 -0
- data/lib/hashie/extensions/ruby_version_check.rb +21 -0
- data/lib/hashie/extensions/strict_key_access.rb +16 -13
- data/lib/hashie/extensions/stringify_keys.rb +1 -1
- data/lib/hashie/extensions/symbolize_keys.rb +13 -2
- data/lib/hashie/hash.rb +18 -11
- data/lib/hashie/logger.rb +18 -0
- data/lib/hashie/mash.rb +177 -43
- data/lib/hashie/railtie.rb +21 -0
- data/lib/hashie/rash.rb +7 -7
- data/lib/hashie/utils.rb +44 -0
- data/lib/hashie/version.rb +1 -1
- data/lib/hashie.rb +33 -17
- metadata +28 -95
- data/spec/hashie/clash_spec.rb +0 -48
- data/spec/hashie/dash_spec.rb +0 -513
- data/spec/hashie/extensions/autoload_spec.rb +0 -24
- data/spec/hashie/extensions/coercion_spec.rb +0 -625
- data/spec/hashie/extensions/dash/coercion_spec.rb +0 -13
- data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
- data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
- data/spec/hashie/extensions/deep_find_spec.rb +0 -45
- data/spec/hashie/extensions/deep_locate_spec.rb +0 -124
- data/spec/hashie/extensions/deep_merge_spec.rb +0 -65
- data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -46
- data/spec/hashie/extensions/indifferent_access_spec.rb +0 -219
- data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
- data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
- data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -50
- data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
- data/spec/hashie/extensions/method_access_spec.rb +0 -184
- data/spec/hashie/extensions/strict_key_access_spec.rb +0 -110
- data/spec/hashie/extensions/stringify_keys_spec.rb +0 -124
- data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -129
- data/spec/hashie/hash_spec.rb +0 -84
- data/spec/hashie/mash_spec.rb +0 -680
- data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -29
- data/spec/hashie/rash_spec.rb +0 -77
- data/spec/hashie/trash_spec.rb +0 -268
- data/spec/hashie/version_spec.rb +0 -7
- data/spec/spec_helper.rb +0 -16
- data/spec/support/module_context.rb +0 -11
@@ -7,10 +7,32 @@ module Hashie
|
|
7
7
|
base.send :include, Hashie::Extensions::IndifferentAccess
|
8
8
|
end
|
9
9
|
|
10
|
+
def self.maybe_extend(base)
|
11
|
+
return unless requires_class_methods?(base)
|
12
|
+
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.requires_class_methods?(klass)
|
17
|
+
klass <= Hashie::Dash &&
|
18
|
+
!klass.singleton_class.included_modules.include?(ClassMethods)
|
19
|
+
end
|
20
|
+
private_class_method :requires_class_methods?
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
defaults = ::Hash[self.class.properties.map do |prop|
|
24
|
+
[Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]]
|
25
|
+
end]
|
26
|
+
|
27
|
+
defaults.merge(self)
|
28
|
+
end
|
29
|
+
alias to_hash to_h
|
30
|
+
|
10
31
|
module ClassMethods
|
11
32
|
# Check to see if the specified property has already been
|
12
33
|
# defined.
|
13
34
|
def property?(name)
|
35
|
+
name = translations[name.to_sym] if translation_for?(name)
|
14
36
|
name = name.to_s
|
15
37
|
!!properties.find { |property| property.to_s == name }
|
16
38
|
end
|
@@ -21,7 +43,7 @@ module Hashie
|
|
21
43
|
end
|
22
44
|
|
23
45
|
def transformed_property(property_name, value)
|
24
|
-
transform = transforms[property_name] || transforms[
|
46
|
+
transform = transforms[property_name] || transforms[property_name.to_sym]
|
25
47
|
transform.call(value)
|
26
48
|
end
|
27
49
|
|
@@ -29,6 +51,13 @@ module Hashie
|
|
29
51
|
name = name.to_s
|
30
52
|
!!transforms.keys.find { |key| key.to_s == name }
|
31
53
|
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def translation_for?(name)
|
58
|
+
included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
|
59
|
+
translation_exists?(name)
|
60
|
+
end
|
32
61
|
end
|
33
62
|
end
|
34
63
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module Dash
|
4
|
+
# Extends a Dash with the ability to accept only predefined values on a property.
|
5
|
+
#
|
6
|
+
# == Example
|
7
|
+
#
|
8
|
+
# class PersonHash < Hashie::Dash
|
9
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
10
|
+
#
|
11
|
+
# property :gender, values: [:male, :female, :prefer_not_to_say]
|
12
|
+
# property :age, values: (0..150) # a Range
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# person = PersonHash.new(gender: :male, age: -1)
|
16
|
+
# # => ArgumentError: The value '-1' is not accepted for property 'age'
|
17
|
+
module PredefinedValues
|
18
|
+
def self.included(base)
|
19
|
+
base.instance_variable_set(:@values_for_properties, {})
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
base.include(InstanceMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
attr_reader :values_for_properties
|
26
|
+
|
27
|
+
def inherited(klass)
|
28
|
+
super
|
29
|
+
klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
|
30
|
+
end
|
31
|
+
|
32
|
+
def property(property_name, options = {})
|
33
|
+
super
|
34
|
+
|
35
|
+
return unless (predefined_values = options[:values])
|
36
|
+
|
37
|
+
assert_predefined_values!(predefined_values)
|
38
|
+
set_predefined_values(property_name, predefined_values)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def assert_predefined_values!(predefined_values)
|
44
|
+
return if supported_type?(predefined_values)
|
45
|
+
|
46
|
+
raise ArgumentError, %(`values` accepts an Array or a Range.)
|
47
|
+
end
|
48
|
+
|
49
|
+
def supported_type?(predefined_values)
|
50
|
+
[::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_predefined_values(property_name, predefined_values)
|
54
|
+
@values_for_properties[property_name] = predefined_values
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
def initialize(*)
|
60
|
+
super
|
61
|
+
|
62
|
+
assert_property_values!
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def assert_property_values!
|
68
|
+
self.class.values_for_properties.each_key do |property|
|
69
|
+
value = send(property)
|
70
|
+
|
71
|
+
if value && !values_for_properties(property).include?(value)
|
72
|
+
fail_property_value_error!(property)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def fail_property_value_error!(property)
|
78
|
+
raise ArgumentError, "Invalid value for property '#{property}'"
|
79
|
+
end
|
80
|
+
|
81
|
+
def values_for_properties(property)
|
82
|
+
self.class.values_for_properties[property]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -35,12 +35,12 @@ module Hashie
|
|
35
35
|
# end
|
36
36
|
#
|
37
37
|
# model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
|
38
|
-
# model.id.class #=> Fixnum
|
38
|
+
# model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
|
39
39
|
# model.created_at.class #=> Time
|
40
40
|
module PropertyTranslation
|
41
41
|
def self.included(base)
|
42
42
|
base.instance_variable_set(:@transforms, {})
|
43
|
-
base.instance_variable_set(:@translations_hash, {})
|
43
|
+
base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
|
44
44
|
base.extend(ClassMethods)
|
45
45
|
base.send(:include, InstanceMethods)
|
46
46
|
end
|
@@ -58,7 +58,9 @@ module Hashie
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def permitted_input_keys
|
61
|
-
@permitted_input_keys ||=
|
61
|
+
@permitted_input_keys ||=
|
62
|
+
properties
|
63
|
+
.map { |property| inverse_translations.fetch property, property }
|
62
64
|
end
|
63
65
|
|
64
66
|
# Defines a property on the Trash. Options are as follows:
|
@@ -72,23 +74,16 @@ module Hashie
|
|
72
74
|
def property(property_name, options = {})
|
73
75
|
super
|
74
76
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
translations_hash[options[:from]] ||= {}
|
81
|
-
translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
|
77
|
+
from = options[:from]
|
78
|
+
converter = options[:with]
|
79
|
+
transformer = options[:transform_with]
|
82
80
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
if options[:transform_with].respond_to? :call
|
90
|
-
transforms[property_name] = options[:transform_with]
|
91
|
-
end
|
81
|
+
if from
|
82
|
+
fail_self_transformation_error!(property_name) if property_name == from
|
83
|
+
define_translation(from, property_name, converter || transformer)
|
84
|
+
define_writer_for_source_property(from)
|
85
|
+
elsif valid_transformer?(transformer)
|
86
|
+
transforms[property_name] = transformer
|
92
87
|
end
|
93
88
|
end
|
94
89
|
|
@@ -105,26 +100,50 @@ module Hashie
|
|
105
100
|
end
|
106
101
|
|
107
102
|
def translations
|
108
|
-
@translations ||= {}.tap do |
|
103
|
+
@translations ||= {}.tap do |translations|
|
109
104
|
translations_hash.each do |(property_name, property_translations)|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
105
|
+
translations[property_name] =
|
106
|
+
if property_translations.size > 1
|
107
|
+
property_translations.keys
|
108
|
+
else
|
109
|
+
property_translations.keys.first
|
110
|
+
end
|
115
111
|
end
|
116
112
|
end
|
117
113
|
end
|
118
114
|
|
119
115
|
def inverse_translations
|
120
|
-
@inverse_translations ||= {}.tap do |
|
116
|
+
@inverse_translations ||= {}.tap do |translations|
|
121
117
|
translations_hash.each do |(property_name, property_translations)|
|
122
|
-
property_translations.
|
123
|
-
|
118
|
+
property_translations.each_key do |key|
|
119
|
+
translations[key] = property_name
|
124
120
|
end
|
125
121
|
end
|
126
122
|
end
|
127
123
|
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def define_translation(from, property_name, translator)
|
128
|
+
translations_hash[from][property_name] = translator
|
129
|
+
end
|
130
|
+
|
131
|
+
def define_writer_for_source_property(property)
|
132
|
+
define_method "#{property}=" do |val|
|
133
|
+
__translations[property].each do |name, with|
|
134
|
+
self[name] = with.respond_to?(:call) ? with.call(val) : val
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def fail_self_transformation_error!(property_name)
|
140
|
+
raise ArgumentError,
|
141
|
+
"Property name (#{property_name}) and :from option must not be the same"
|
142
|
+
end
|
143
|
+
|
144
|
+
def valid_transformer?(transformer)
|
145
|
+
transformer.respond_to? :call
|
146
|
+
end
|
128
147
|
end
|
129
148
|
|
130
149
|
module InstanceMethods
|
@@ -134,6 +153,12 @@ module Hashie
|
|
134
153
|
def []=(property, value)
|
135
154
|
if self.class.translation_exists? property
|
136
155
|
send("#{property}=", value)
|
156
|
+
|
157
|
+
if self.class.transformation_exists? property
|
158
|
+
super property, self.class.transformed_property(property, value)
|
159
|
+
elsif self.class.properties.include?(property)
|
160
|
+
super(property, value)
|
161
|
+
end
|
137
162
|
elsif self.class.transformation_exists? property
|
138
163
|
super property, self.class.transformed_property(property, value)
|
139
164
|
elsif property_exists? property
|
@@ -158,6 +183,12 @@ module Hashie
|
|
158
183
|
fail_no_property_error!(property) unless self.class.property?(property)
|
159
184
|
true
|
160
185
|
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def __translations
|
190
|
+
self.class.translations_hash
|
191
|
+
end
|
161
192
|
end
|
162
193
|
end
|
163
194
|
end
|
@@ -9,7 +9,8 @@ module Hashie
|
|
9
9
|
#
|
10
10
|
# options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
|
11
11
|
#
|
12
|
-
# This is particularly useful for fetching values from deeply nested api responses
|
12
|
+
# This is particularly useful for fetching values from deeply nested api responses
|
13
|
+
# or params hashes.
|
13
14
|
module DeepFetch
|
14
15
|
class UndefinedPathError < StandardError; end
|
15
16
|
|
@@ -19,8 +20,9 @@ module Hashie
|
|
19
20
|
arg = Integer(arg) if obj.is_a? Array
|
20
21
|
obj.fetch(arg)
|
21
22
|
rescue ArgumentError, IndexError, NoMethodError => e
|
22
|
-
break
|
23
|
-
raise UndefinedPathError,
|
23
|
+
break yield(arg) if block
|
24
|
+
raise UndefinedPathError,
|
25
|
+
"Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'hashie/extensions/deep_locate'
|
1
2
|
module Hashie
|
2
3
|
module Extensions
|
3
4
|
module DeepFind
|
@@ -19,12 +20,17 @@ module Hashie
|
|
19
20
|
_deep_find(key)
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
+
alias deep_detect deep_find
|
23
24
|
|
24
25
|
# Performs a depth-first search on deeply nested data structures for
|
25
26
|
# a key and returns all occurrences of the key.
|
26
27
|
#
|
27
|
-
# options = {
|
28
|
+
# options = {
|
29
|
+
# users: [
|
30
|
+
# { location: {address: '123 Street'} },
|
31
|
+
# { location: {address: '234 Street'}}
|
32
|
+
# ]
|
33
|
+
# }
|
28
34
|
# options.extend(Hashie::Extensions::DeepFind)
|
29
35
|
# options.deep_find_all(:address) # => ['123 Street', '234 Street']
|
30
36
|
#
|
@@ -33,14 +39,17 @@ module Hashie
|
|
33
39
|
# end
|
34
40
|
#
|
35
41
|
# my_hash = MyHash.new
|
36
|
-
# my_hash[:users] = [
|
42
|
+
# my_hash[:users] = [
|
43
|
+
# {location: {address: '123 Street'}},
|
44
|
+
# {location: {address: '234 Street'}}
|
45
|
+
# ]
|
37
46
|
# my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
|
38
47
|
def deep_find_all(key)
|
39
48
|
matches = _deep_find_all(key)
|
40
49
|
matches.empty? ? nil : matches
|
41
50
|
end
|
42
51
|
|
43
|
-
|
52
|
+
alias deep_select deep_find_all
|
44
53
|
|
45
54
|
private
|
46
55
|
|
@@ -49,7 +58,7 @@ module Hashie
|
|
49
58
|
end
|
50
59
|
|
51
60
|
def _deep_find_all(key, object = self, matches = [])
|
52
|
-
deep_locate_result =
|
61
|
+
deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
|
53
62
|
result.map! { |element| element[key] }
|
54
63
|
end
|
55
64
|
|
@@ -14,14 +14,11 @@ module Hashie
|
|
14
14
|
# ...
|
15
15
|
# ]
|
16
16
|
#
|
17
|
-
#
|
17
|
+
# DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
|
18
18
|
# # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
|
19
19
|
def self.deep_locate(comparator, object)
|
20
|
-
# ensure comparator is a callable
|
21
20
|
unless comparator.respond_to?(:call)
|
22
|
-
comparator =
|
23
|
-
->(key, _, _) { key == non_callable_object }
|
24
|
-
end.call(comparator)
|
21
|
+
comparator = _construct_key_comparator(comparator, object)
|
25
22
|
end
|
26
23
|
|
27
24
|
_deep_locate(comparator, object)
|
@@ -58,37 +55,59 @@ module Hashie
|
|
58
55
|
# # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
|
59
56
|
#
|
60
57
|
# books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
|
61
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
58
|
+
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
59
|
+
# # {:title=>"Ruby for the rest of us", :pages=>576}]
|
62
60
|
#
|
63
61
|
# books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
|
64
|
-
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
62
|
+
# # => [{:title=>"Ruby for beginners", :pages=>120},
|
63
|
+
# # {:title=>"CSS for intermediates", :pages=>80}]
|
65
64
|
def deep_locate(comparator)
|
66
65
|
Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
|
67
66
|
end
|
68
67
|
|
69
|
-
|
68
|
+
def self._construct_key_comparator(search_key, object)
|
69
|
+
if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
|
70
|
+
activesupport_indifferent?(object)
|
71
|
+
search_key = search_key.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
lambda do |non_callable_object|
|
75
|
+
->(key, _, _) { key == non_callable_object }
|
76
|
+
end.call(search_key)
|
77
|
+
end
|
78
|
+
private_class_method :_construct_key_comparator
|
70
79
|
|
71
80
|
def self._deep_locate(comparator, object, result = [])
|
72
81
|
if object.is_a?(::Enumerable)
|
73
|
-
if object.any?
|
74
|
-
if object.is_a?(::Hash)
|
75
|
-
key, value = value
|
76
|
-
else
|
77
|
-
key = nil
|
78
|
-
end
|
79
|
-
|
80
|
-
comparator.call(key, value, object)
|
81
|
-
end
|
82
|
+
if object.any? { |value| _match_comparator?(value, comparator, object) }
|
82
83
|
result.push object
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
84
|
+
end
|
85
|
+
|
86
|
+
(object.respond_to?(:values) ? object.values : object.entries).each do |value|
|
87
|
+
_deep_locate(comparator, value, result)
|
87
88
|
end
|
88
89
|
end
|
89
90
|
|
90
91
|
result
|
91
92
|
end
|
93
|
+
private_class_method :_deep_locate
|
94
|
+
|
95
|
+
def self._match_comparator?(value, comparator, object)
|
96
|
+
if object.is_a?(::Hash)
|
97
|
+
key, value = value
|
98
|
+
else
|
99
|
+
key = nil
|
100
|
+
end
|
101
|
+
|
102
|
+
comparator.call(key, value, object)
|
103
|
+
end
|
104
|
+
private_class_method :_match_comparator?
|
105
|
+
|
106
|
+
def self.activesupport_indifferent?(object)
|
107
|
+
defined?(::ActiveSupport::HashWithIndifferentAccess) &&
|
108
|
+
object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
|
109
|
+
end
|
110
|
+
private_class_method :activesupport_indifferent?
|
92
111
|
end
|
93
112
|
end
|
94
113
|
end
|
@@ -3,7 +3,7 @@ module Hashie
|
|
3
3
|
module DeepMerge
|
4
4
|
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
5
5
|
def deep_merge(other_hash, &block)
|
6
|
-
copy =
|
6
|
+
copy = _deep_dup(self)
|
7
7
|
copy.extend(Hashie::Extensions::DeepMerge) unless copy.respond_to?(:deep_merge!)
|
8
8
|
copy.deep_merge!(other_hash, &block)
|
9
9
|
end
|
@@ -18,17 +18,33 @@ module Hashie
|
|
18
18
|
|
19
19
|
private
|
20
20
|
|
21
|
+
def _deep_dup(hash)
|
22
|
+
copy = hash.dup
|
23
|
+
|
24
|
+
copy.each do |key, value|
|
25
|
+
copy[key] =
|
26
|
+
if value.is_a?(::Hash)
|
27
|
+
_deep_dup(value)
|
28
|
+
else
|
29
|
+
Hashie::Utils.safe_dup(value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
copy
|
34
|
+
end
|
35
|
+
|
21
36
|
def _recursive_merge(hash, other_hash, &block)
|
22
37
|
other_hash.each do |k, v|
|
23
|
-
hash[k] =
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
38
|
+
hash[k] =
|
39
|
+
if hash.key?(k) && hash[k].is_a?(::Hash) && v.is_a?(::Hash)
|
40
|
+
_recursive_merge(hash[k], v, &block)
|
41
|
+
elsif v.is_a?(::Hash)
|
42
|
+
_recursive_merge({}, v, &block)
|
43
|
+
elsif hash.key?(k) && block_given?
|
44
|
+
yield(k, hash[k], v)
|
45
|
+
else
|
46
|
+
v.respond_to?(:deep_dup) ? v.deep_dup : v
|
47
|
+
end
|
32
48
|
end
|
33
49
|
hash
|
34
50
|
end
|
@@ -30,10 +30,12 @@ module Hashie
|
|
30
30
|
# p.email # => NoMethodError
|
31
31
|
module IgnoreUndeclared
|
32
32
|
def initialize_attributes(attributes)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
33
|
+
return unless attributes
|
34
|
+
|
35
|
+
klass = self.class
|
36
|
+
translations = klass.respond_to?(:translations) && klass.translations || []
|
37
|
+
|
38
|
+
super(attributes.select { |attr, _| klass.property?(attr) || translations.include?(attr) })
|
37
39
|
end
|
38
40
|
|
39
41
|
def property_exists?(property)
|
@@ -23,21 +23,26 @@ module Hashie
|
|
23
23
|
# h['baz'] # => 'blip'
|
24
24
|
#
|
25
25
|
module IndifferentAccess
|
26
|
+
include Hashie::Extensions::RubyVersionCheck
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
def self.convert_key(key)
|
30
|
+
key.to_s
|
31
|
+
end
|
32
|
+
|
26
33
|
def self.included(base)
|
27
|
-
Hashie::Extensions::Dash::IndifferentAccess
|
28
|
-
base.extend(extension) if base <= Hashie::Dash && !base.singleton_class.included_modules.include?(extension)
|
29
|
-
end
|
34
|
+
Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(base)
|
30
35
|
|
31
36
|
base.class_eval do
|
32
37
|
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
33
38
|
alias_method :[]=, :indifferent_writer
|
34
39
|
alias_method :store, :indifferent_writer
|
35
|
-
%w
|
40
|
+
%w[default update replace fetch delete key? values_at].each do |m|
|
36
41
|
alias_method "regular_#{m}", m unless method_defined?("regular_#{m}")
|
37
42
|
alias_method m, "indifferent_#{m}"
|
38
43
|
end
|
39
44
|
|
40
|
-
%w
|
45
|
+
%w[include? member? has_key?].each do |key_alias|
|
41
46
|
alias_method key_alias, :indifferent_key?
|
42
47
|
end
|
43
48
|
|
@@ -68,15 +73,15 @@ module Hashie
|
|
68
73
|
end
|
69
74
|
|
70
75
|
def convert_key(key)
|
71
|
-
key
|
76
|
+
IndifferentAccess.convert_key(key)
|
72
77
|
end
|
73
78
|
|
74
79
|
# Iterates through the keys and values, reconverting them to
|
75
80
|
# their proper indifferent state. Used when IndifferentAccess
|
76
81
|
# is injecting itself into member hashes.
|
77
82
|
def convert!
|
78
|
-
keys.each do |k|
|
79
|
-
|
83
|
+
keys.each do |k| # rubocop:disable Performance/HashEachMethods
|
84
|
+
indifferent_writer k, regular_delete(k)
|
80
85
|
end
|
81
86
|
self
|
82
87
|
end
|
@@ -133,6 +138,42 @@ module Hashie
|
|
133
138
|
self
|
134
139
|
end
|
135
140
|
|
141
|
+
def merge(*args)
|
142
|
+
result = super
|
143
|
+
return IndifferentAccess.inject!(result) if hash_lacking_indifference?(result)
|
144
|
+
result.convert!
|
145
|
+
end
|
146
|
+
|
147
|
+
def merge!(*)
|
148
|
+
super.convert!
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_hash
|
152
|
+
{}.tap do |result|
|
153
|
+
each_pair { |key, value| result[key] = value }
|
154
|
+
|
155
|
+
if default_proc
|
156
|
+
result.default_proc = default_proc
|
157
|
+
else
|
158
|
+
result.default = default
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
with_minimum_ruby('2.5.0') do
|
164
|
+
def slice(*keys)
|
165
|
+
string_keys = keys.map { |key| convert_key(key) }
|
166
|
+
super(*string_keys)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
with_minimum_ruby('3.0.0') do
|
171
|
+
def except(*keys)
|
172
|
+
string_keys = keys.map { |key| convert_key(key) }
|
173
|
+
super(*string_keys)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
136
177
|
protected
|
137
178
|
|
138
179
|
def hash_lacking_indifference?(other)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module KeyConflictWarning
|
4
|
+
class CannotDisableMashWarnings < StandardError
|
5
|
+
def initialize
|
6
|
+
super(
|
7
|
+
'You cannot disable warnings on the base Mash class. ' \
|
8
|
+
'Please subclass the Mash and disable it in the subclass.'
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Disable the logging of warnings based on keys conflicting keys/methods
|
14
|
+
#
|
15
|
+
# @api semipublic
|
16
|
+
# @return [void]
|
17
|
+
def disable_warnings(*method_keys)
|
18
|
+
raise CannotDisableMashWarnings if self == Hashie::Mash
|
19
|
+
if method_keys.any?
|
20
|
+
disabled_warnings.concat(method_keys).tap(&:flatten!).uniq!
|
21
|
+
else
|
22
|
+
disabled_warnings.clear
|
23
|
+
end
|
24
|
+
|
25
|
+
@disable_warnings = true
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks whether this class disables warnings for conflicting keys/methods
|
29
|
+
#
|
30
|
+
# @api semipublic
|
31
|
+
# @return [Boolean]
|
32
|
+
def disable_warnings?(method_key = nil)
|
33
|
+
return disabled_warnings.include?(method_key) if disabled_warnings.any? && method_key
|
34
|
+
@disable_warnings ||= false
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns an array of methods that this class disables warnings for.
|
38
|
+
#
|
39
|
+
# @api semipublic
|
40
|
+
# @return [Boolean]
|
41
|
+
def disabled_warnings
|
42
|
+
@_disabled_warnings ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
# Inheritance hook that sets class configuration when inherited.
|
46
|
+
#
|
47
|
+
# @api semipublic
|
48
|
+
# @return [void]
|
49
|
+
def inherited(subclass)
|
50
|
+
super
|
51
|
+
subclass.disable_warnings(disabled_warnings) if disable_warnings?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|