hashie 3.4.2 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +518 -122
- data/CONTRIBUTING.md +24 -7
- data/LICENSE +1 -1
- data/README.md +455 -48
- data/Rakefile +18 -1
- data/UPGRADING.md +157 -7
- data/hashie.gemspec +14 -7
- 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 +91 -52
- data/lib/hashie/extensions/dash/coercion.rb +25 -0
- 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 -30
- 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 +28 -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 +77 -0
- data/lib/hashie/extensions/stringify_keys.rb +8 -5
- data/lib/hashie/extensions/symbolize_keys.rb +21 -7
- data/lib/hashie/hash.rb +18 -11
- data/lib/hashie/logger.rb +18 -0
- data/lib/hashie/mash.rb +196 -55
- 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 +34 -16
- metadata +30 -79
- 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/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 -45
- 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 -23
- data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
- data/spec/hashie/extensions/method_access_spec.rb +0 -184
- data/spec/hashie/extensions/stringify_keys_spec.rb +0 -101
- data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -106
- data/spec/hashie/hash_spec.rb +0 -84
- data/spec/hashie/mash_spec.rb +0 -683
- 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 -15
- data/spec/support/module_context.rb +0 -11
- data/spec/support/ruby_version.rb +0 -10
@@ -1,5 +1,9 @@
|
|
1
1
|
module Hashie
|
2
|
-
class CoercionError < StandardError
|
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
|
-
|
17
|
-
|
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
|
23
|
-
|
24
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
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 |
|
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[
|
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,25 +74,16 @@ module Hashie
|
|
72
74
|
def property(property_name, options = {})
|
73
75
|
super
|
74
76
|
|
75
|
-
|
77
|
+
from = options[:from]
|
78
|
+
converter = options[:with]
|
79
|
+
transformer = options[:transform_with]
|
76
80
|
|
77
|
-
if
|
78
|
-
if property_name ==
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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 |
|
103
|
+
@translations ||= {}.tap do |translations|
|
111
104
|
translations_hash.each do |(property_name, property_translations)|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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 |
|
116
|
+
@inverse_translations ||= {}.tap do |translations|
|
123
117
|
translations_hash.each do |(property_name, property_translations)|
|
124
|
-
property_translations.
|
125
|
-
|
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
|
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
|