hashie 3.4.2 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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