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.
- 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
|