hashie 3.4.2 → 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.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +518 -122
  3. data/CONTRIBUTING.md +24 -7
  4. data/LICENSE +1 -1
  5. data/README.md +455 -48
  6. data/Rakefile +18 -1
  7. data/UPGRADING.md +157 -7
  8. data/hashie.gemspec +14 -7
  9. data/lib/hashie/array.rb +21 -0
  10. data/lib/hashie/clash.rb +24 -12
  11. data/lib/hashie/dash.rb +56 -31
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +19 -0
  14. data/lib/hashie/extensions/coercion.rb +91 -52
  15. data/lib/hashie/extensions/dash/coercion.rb +25 -0
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +30 -1
  17. data/lib/hashie/extensions/dash/predefined_values.rb +88 -0
  18. data/lib/hashie/extensions/dash/property_translation.rb +59 -30
  19. data/lib/hashie/extensions/deep_fetch.rb +5 -3
  20. data/lib/hashie/extensions/deep_find.rb +14 -5
  21. data/lib/hashie/extensions/deep_locate.rb +40 -21
  22. data/lib/hashie/extensions/deep_merge.rb +28 -10
  23. data/lib/hashie/extensions/ignore_undeclared.rb +6 -4
  24. data/lib/hashie/extensions/indifferent_access.rb +49 -8
  25. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  26. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  27. data/lib/hashie/extensions/mash/keep_original_keys.rb +53 -0
  28. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  29. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  30. data/lib/hashie/extensions/mash/symbolize_keys.rb +38 -0
  31. data/lib/hashie/extensions/method_access.rb +77 -19
  32. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +29 -5
  33. data/lib/hashie/extensions/ruby_version.rb +60 -0
  34. data/lib/hashie/extensions/ruby_version_check.rb +21 -0
  35. data/lib/hashie/extensions/strict_key_access.rb +77 -0
  36. data/lib/hashie/extensions/stringify_keys.rb +8 -5
  37. data/lib/hashie/extensions/symbolize_keys.rb +21 -7
  38. data/lib/hashie/hash.rb +18 -11
  39. data/lib/hashie/logger.rb +18 -0
  40. data/lib/hashie/mash.rb +196 -55
  41. data/lib/hashie/railtie.rb +21 -0
  42. data/lib/hashie/rash.rb +7 -7
  43. data/lib/hashie/utils.rb +44 -0
  44. data/lib/hashie/version.rb +1 -1
  45. data/lib/hashie.rb +34 -16
  46. metadata +30 -79
  47. data/spec/hashie/clash_spec.rb +0 -48
  48. data/spec/hashie/dash_spec.rb +0 -513
  49. data/spec/hashie/extensions/autoload_spec.rb +0 -24
  50. data/spec/hashie/extensions/coercion_spec.rb +0 -625
  51. data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
  52. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
  53. data/spec/hashie/extensions/deep_find_spec.rb +0 -45
  54. data/spec/hashie/extensions/deep_locate_spec.rb +0 -124
  55. data/spec/hashie/extensions/deep_merge_spec.rb +0 -45
  56. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -46
  57. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -219
  58. data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
  59. data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
  60. data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -23
  61. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  62. data/spec/hashie/extensions/method_access_spec.rb +0 -184
  63. data/spec/hashie/extensions/stringify_keys_spec.rb +0 -101
  64. data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -106
  65. data/spec/hashie/hash_spec.rb +0 -84
  66. data/spec/hashie/mash_spec.rb +0 -683
  67. data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -29
  68. data/spec/hashie/rash_spec.rb +0 -77
  69. data/spec/hashie/trash_spec.rb +0 -268
  70. data/spec/hashie/version_spec.rb +0 -7
  71. data/spec/spec_helper.rb +0 -15
  72. data/spec/support/module_context.rb +0 -11
  73. data/spec/support/ruby_version.rb +0 -10
@@ -1,5 +1,9 @@
1
1
  module Hashie
2
- class CoercionError < StandardError; end
2
+ class CoercionError < StandardError
3
+ def initialize(key, value, into, message)
4
+ super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
5
+ end
6
+ end
3
7
 
4
8
  module Extensions
5
9
  module Coercion
@@ -10,18 +14,24 @@ module Hashie
10
14
  Rational => :to_r,
11
15
  String => :to_s,
12
16
  Symbol => :to_sym
13
- }
14
-
15
- ABSTRACT_CORE_TYPES = {
16
- Integer => [Fixnum, Bignum],
17
- Numeric => [Fixnum, Bignum, Float, Complex, Rational]
18
- }
17
+ }.freeze
18
+
19
+ ABSTRACT_CORE_TYPES =
20
+ if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
21
+ { Numeric => [Integer, Float, Complex, Rational] }
22
+ else
23
+ {
24
+ Integer => [Fixnum, Bignum],
25
+ Numeric => [Fixnum, Bignum, Float, Complex, Rational]
26
+ }
27
+ end
19
28
 
20
29
  def self.included(base)
21
30
  base.send :include, InstanceMethods
22
- base.extend ClassMethods # NOTE: we wanna make sure we first define set_value_with_coercion before extending
23
-
24
- base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
31
+ base.extend ClassMethods
32
+ unless base.method_defined?(:set_value_without_coercion)
33
+ base.send :alias_method, :set_value_without_coercion, :[]=
34
+ end
25
35
  base.send :alias_method, :[]=, :set_value_with_coercion
26
36
  end
27
37
 
@@ -29,51 +39,17 @@ module Hashie
29
39
  def set_value_with_coercion(key, value)
30
40
  into = self.class.key_coercion(key) || self.class.value_coercion(value)
31
41
 
32
- return set_value_without_coercion(key, value) if value.nil? || into.nil?
33
-
34
- begin
35
- return set_value_without_coercion(key, coerce_or_init(into).call(value)) unless into.is_a?(Enumerable)
36
-
37
- if into.class <= ::Hash
38
- key_coerce = coerce_or_init(into.flatten[0])
39
- value_coerce = coerce_or_init(into.flatten[-1])
40
- value = into.class[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
41
- else # Enumerable but not Hash: Array, Set
42
- value_coerce = coerce_or_init(into.first)
43
- value = into.class.new(value.map { |v| value_coerce.call(v) })
42
+ unless value.nil? || into.nil?
43
+ begin
44
+ value = self.class.fetch_coercion(into).call(value)
45
+ rescue NoMethodError, TypeError => e
46
+ raise CoercionError.new(key, value, into, e.message)
44
47
  end
45
- rescue NoMethodError, TypeError => e
46
- raise CoercionError, "Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{e.message}"
47
48
  end
48
49
 
49
50
  set_value_without_coercion(key, value)
50
51
  end
51
52
 
52
- def coerce_or_init(type)
53
- return type if type.is_a? Proc
54
-
55
- if CORE_TYPES.key?(type)
56
- lambda do |v|
57
- return v if v.is_a? type
58
- return v.send(CORE_TYPES[type])
59
- end
60
- elsif type.respond_to?(:coerce)
61
- lambda do |v|
62
- return v if v.is_a? type
63
- type.coerce(v)
64
- end
65
- elsif type.respond_to?(:new)
66
- lambda do |v|
67
- return v if v.is_a? type
68
- type.new(v)
69
- end
70
- else
71
- fail TypeError, "#{type} is not a coercable type"
72
- end
73
- end
74
-
75
- private :coerce_or_init
76
-
77
53
  def custom_writer(key, value, _convert = true)
78
54
  self[key] = value
79
55
  end
@@ -108,7 +84,7 @@ module Hashie
108
84
  attrs.each { |key| key_coercions[key] = into }
109
85
  end
110
86
 
111
- alias_method :coerce_keys, :coerce_key
87
+ alias coerce_keys coerce_key
112
88
 
113
89
  # Returns a hash of any existing key coercions.
114
90
  def key_coercions
@@ -127,7 +103,8 @@ module Hashie
127
103
  #
128
104
  # @param [Class] from the type you would like coerced.
129
105
  # @param [Class] into the class into which you would like the value coerced.
130
- # @option options [Boolean] :strict (true) whether use exact source class only or include ancestors
106
+ # @option options [Boolean] :strict (true) whether use exact source class
107
+ # only or include ancestors
131
108
  #
132
109
  # @example Coerce all hashes into this special type of hash
133
110
  # class SpecialHash < Hash
@@ -145,7 +122,7 @@ module Hashie
145
122
  options = { strict: true }.merge(options)
146
123
 
147
124
  if ABSTRACT_CORE_TYPES.key? from
148
- ABSTRACT_CORE_TYPES[from].each do | type |
125
+ ABSTRACT_CORE_TYPES[from].each do |type|
149
126
  coerce_value type, into, options
150
127
  end
151
128
  end
@@ -164,6 +141,7 @@ module Hashie
164
141
  def strict_value_coercions
165
142
  @strict_value_coercions ||= {}
166
143
  end
144
+
167
145
  # Return all value coercions that have the :strict rule as false.
168
146
  def lenient_value_coercions
169
147
  @lenient_value_coercions ||= {}
@@ -175,6 +153,67 @@ module Hashie
175
153
  strict_value_coercions[from] || lenient_value_coercions[from]
176
154
  end
177
155
 
156
+ def fetch_coercion(type)
157
+ return type if type.is_a? Proc
158
+ coercion_cache[type]
159
+ end
160
+
161
+ def coercion_cache
162
+ @coercion_cache ||= ::Hash.new do |hash, type|
163
+ hash[type] = build_coercion(type)
164
+ end
165
+ end
166
+
167
+ def build_coercion(type)
168
+ if type.is_a? Enumerable
169
+ if type.class == ::Hash
170
+ type, key_type, value_type = type.class, *type.first
171
+ build_hash_coercion(type, key_type, value_type)
172
+ else
173
+ value_type = type.first
174
+ type = type.class
175
+ build_container_coercion(type, value_type)
176
+ end
177
+ elsif CORE_TYPES.key? type
178
+ build_core_type_coercion(type)
179
+ elsif type.respond_to? :coerce
180
+ lambda do |value|
181
+ return value if value.is_a? type
182
+ type.coerce(value)
183
+ end
184
+ elsif type.respond_to? :new
185
+ lambda do |value|
186
+ return value if value.is_a? type
187
+ type.new(value)
188
+ end
189
+ else
190
+ raise TypeError, "#{type} is not a coercable type"
191
+ end
192
+ end
193
+
194
+ def build_hash_coercion(type, key_type, value_type)
195
+ key_coerce = fetch_coercion(key_type)
196
+ value_coerce = fetch_coercion(value_type)
197
+ lambda do |value|
198
+ type[value.map { |k, v| [key_coerce.call(k), value_coerce.call(v)] }]
199
+ end
200
+ end
201
+
202
+ def build_container_coercion(type, value_type)
203
+ value_coerce = fetch_coercion(value_type)
204
+ lambda do |value|
205
+ type.new(value.map { |v| value_coerce.call(v) })
206
+ end
207
+ end
208
+
209
+ def build_core_type_coercion(type)
210
+ name = CORE_TYPES[type]
211
+ lambda do |value|
212
+ return value if value.is_a? type
213
+ return value.send(name)
214
+ end
215
+ end
216
+
178
217
  def inherited(klass)
179
218
  super
180
219
 
@@ -0,0 +1,25 @@
1
+ module Hashie
2
+ module Extensions
3
+ module Dash
4
+ module Coercion
5
+ # Extends a Dash with the ability to define coercion for properties.
6
+
7
+ def self.included(base)
8
+ base.send :include, Hashie::Extensions::Coercion
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ # Defines a property on the Dash. Options are the standard
14
+ # <tt>Hashie::Dash#property</tt> options plus:
15
+ #
16
+ # * <tt>:coerce</tt> - The class into which you want the property coerced.
17
+ def property(property_name, options = {})
18
+ super
19
+ coerce_key property_name, options[:coerce] if options[:coerce]
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -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[:"#{property_name}"]
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 ||= properties.map { |property| inverse_translations.fetch property, property }
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,25 +74,16 @@ module Hashie
72
74
  def property(property_name, options = {})
73
75
  super
74
76
 
75
- options[:from] = options[:from] if options[:from]
77
+ from = options[:from]
78
+ converter = options[:with]
79
+ transformer = options[:transform_with]
76
80
 
77
- if options[:from]
78
- if property_name == options[:from]
79
- fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
80
- end
81
-
82
- translations_hash[options[:from]] ||= {}
83
- translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
84
-
85
- define_method "#{options[:from]}=" do |val|
86
- self.class.translations_hash[options[:from]].each do |name, with|
87
- self[name] = with.respond_to?(:call) ? with.call(val) : val
88
- end
89
- end
90
- else
91
- if options[:transform_with].respond_to? :call
92
- transforms[property_name] = options[:transform_with]
93
- 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
94
87
  end
95
88
  end
96
89
 
@@ -107,26 +100,50 @@ module Hashie
107
100
  end
108
101
 
109
102
  def translations
110
- @translations ||= {}.tap do |h|
103
+ @translations ||= {}.tap do |translations|
111
104
  translations_hash.each do |(property_name, property_translations)|
112
- if property_translations.size > 1
113
- h[property_name] = property_translations.keys
114
- else
115
- h[property_name] = property_translations.keys.first
116
- end
105
+ translations[property_name] =
106
+ if property_translations.size > 1
107
+ property_translations.keys
108
+ else
109
+ property_translations.keys.first
110
+ end
117
111
  end
118
112
  end
119
113
  end
120
114
 
121
115
  def inverse_translations
122
- @inverse_translations ||= {}.tap do |h|
116
+ @inverse_translations ||= {}.tap do |translations|
123
117
  translations_hash.each do |(property_name, property_translations)|
124
- property_translations.keys.each do |k|
125
- h[k] = property_name
118
+ property_translations.each_key do |key|
119
+ translations[key] = property_name
126
120
  end
127
121
  end
128
122
  end
129
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
130
147
  end
131
148
 
132
149
  module InstanceMethods
@@ -136,6 +153,12 @@ module Hashie
136
153
  def []=(property, value)
137
154
  if self.class.translation_exists? property
138
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
139
162
  elsif self.class.transformation_exists? property
140
163
  super property, self.class.transformed_property(property, value)
141
164
  elsif property_exists? property
@@ -160,6 +183,12 @@ module Hashie
160
183
  fail_no_property_error!(property) unless self.class.property?(property)
161
184
  true
162
185
  end
186
+
187
+ private
188
+
189
+ def __translations
190
+ self.class.translations_hash
191
+ end
163
192
  end
164
193
  end
165
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 or params hashes.
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 block.call(arg) if block
23
- raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
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
- alias_method :deep_detect, :deep_find
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 = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
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] = [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]
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
- alias_method :deep_select, :deep_find_all
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 = Hashie::Extensions::DeepLocate.deep_locate(key, object).tap do |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
- # Hashie::Extensions::DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
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 = lambda do |non_callable_object|
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}, {:title=>"Ruby for the rest of us", :pages=>576}]
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}, {:title=>"CSS for intermediates", :pages=>80}]
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
- private
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? do |value|
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
- else
84
- (object.respond_to?(:values) ? object.values : object.entries).each do |value|
85
- _deep_locate(comparator, value, result)
86
- end
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