hashie 5.0.0 → 5.2.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 +4 -4
- data/CHANGELOG.md +77 -2
- data/CONTRIBUTING.md +38 -1
- data/README.md +849 -213
- data/Rakefile +32 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +10 -5
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +145 -17
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
- data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
- data/lib/hashie/extensions/block_access.rb +71 -0
- data/lib/hashie/extensions/coercion.rb +180 -26
- data/lib/hashie/extensions/dash/coercion.rb +34 -3
- data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
- data/lib/hashie/extensions/dash/lazy.rb +170 -0
- data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
- data/lib/hashie/extensions/dash/property_translation.rb +134 -16
- data/lib/hashie/extensions/deep_fetch.rb +21 -9
- data/lib/hashie/extensions/deep_find.rb +20 -0
- data/lib/hashie/extensions/deep_grep.rb +69 -0
- data/lib/hashie/extensions/deep_locate.rb +16 -29
- data/lib/hashie/extensions/deep_merge.rb +16 -0
- data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
- data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
- data/lib/hashie/extensions/indifferent_access.rb +100 -14
- data/lib/hashie/extensions/kernel_coercion.rb +58 -0
- data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
- data/lib/hashie/extensions/key_conversion.rb +9 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
- data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
- data/lib/hashie/extensions/mash/required_keys.rb +111 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
- data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
- data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
- data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
- data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
- data/lib/hashie/extensions/merge_initializer.rb +9 -0
- data/lib/hashie/extensions/method_access.rb +94 -8
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
- data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
- data/lib/hashie/extensions/persistable.rb +137 -0
- data/lib/hashie/extensions/pretty_inspect.rb +14 -0
- data/lib/hashie/extensions/ruby_version.rb +18 -1
- data/lib/hashie/extensions/ruby_version_check.rb +17 -0
- data/lib/hashie/extensions/strict_key_access.rb +59 -5
- data/lib/hashie/extensions/stringify_keys.rb +72 -17
- data/lib/hashie/extensions/symbolize_keys.rb +72 -16
- data/lib/hashie/hash.rb +16 -0
- data/lib/hashie/logger.rb +18 -1
- data/lib/hashie/mash.rb +302 -75
- data/lib/hashie/railtie.rb +27 -1
- data/lib/hashie/rash.rb +64 -4
- data/lib/hashie/trash.rb +8 -0
- data/lib/hashie/utils.rb +17 -6
- data/lib/hashie/version.rb +3 -1
- data/lib/hashie.rb +45 -0
- metadata +18 -6
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hashie
|
|
4
|
+
module Extensions
|
|
5
|
+
# Builds nested hashes through blocks without changing ordinary method access.
|
|
6
|
+
# Include after MethodAccess when using a Hash subclass.
|
|
7
|
+
#
|
|
8
|
+
# class Configuration < Hashie::Mash
|
|
9
|
+
# include Hashie::Extensions::BlockAccess
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# config = Configuration.new
|
|
13
|
+
# config.author { |author| author.name = 'Michael Bleigh' }
|
|
14
|
+
# config.author.name # => 'Michael Bleigh'
|
|
15
|
+
#
|
|
16
|
+
# @example Build a nested configuration
|
|
17
|
+
# class Configuration < Hashie::Mash
|
|
18
|
+
# include Hashie::Extensions::BlockAccess
|
|
19
|
+
# end
|
|
20
|
+
# config = Configuration.new
|
|
21
|
+
# config.author { |author| author.name = 'Michael' }
|
|
22
|
+
# config.author.name # => 'Michael'
|
|
23
|
+
module BlockAccess
|
|
24
|
+
# @example Build a child through bracket access
|
|
25
|
+
# hash = {}.extend(Hashie::Extensions::BlockAccess)
|
|
26
|
+
# hash.[](:author) { |author| author[:name] = 'Michael' }
|
|
27
|
+
# hash # => { author: { name: 'Michael' } }
|
|
28
|
+
def [](key)
|
|
29
|
+
return super unless block_given?
|
|
30
|
+
|
|
31
|
+
value = block_access_child(self, key)
|
|
32
|
+
yield value
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A block-only builder does not make a missing reader available to respond_to?.
|
|
37
|
+
#
|
|
38
|
+
# @example Build a child through a missing method
|
|
39
|
+
# hash = {}.extend(Hashie::Extensions::BlockAccess)
|
|
40
|
+
# hash.author { |author| author[:name] = 'Michael' }
|
|
41
|
+
# hash['author'] # => { name: 'Michael' }
|
|
42
|
+
def method_missing(name, *args, &block) # rubocop:disable Style/MissingRespondToMissing
|
|
43
|
+
return super unless block && !name.to_s.end_with?('=', '?', '!', '_')
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, "wrong number of arguments (given `#{args.size}`, expected 0..1)" if args.size > 1
|
|
46
|
+
|
|
47
|
+
child = block_access_child(self, block_access_key(name))
|
|
48
|
+
child = block_access_child(child, args.first) unless args.empty?
|
|
49
|
+
block.call(child)
|
|
50
|
+
child
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def block_access_key(name)
|
|
56
|
+
return name if key?(name)
|
|
57
|
+
return name.to_s if key?(name.to_s)
|
|
58
|
+
|
|
59
|
+
respond_to?(:convert_key, true) ? convert_key(name.to_s) : name.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def block_access_child(parent, key)
|
|
63
|
+
parent[key] = parent.class.new unless parent.key?(key)
|
|
64
|
+
child = parent[key]
|
|
65
|
+
return child if child.is_a?(::Hash)
|
|
66
|
+
|
|
67
|
+
raise TypeError, "key `#{key}` already contains a `#{child.class}`"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -1,48 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
4
|
+
# @example Describe a failed conversion
|
|
5
|
+
# require 'hashie/extensions/coercion'
|
|
6
|
+
# error = Hashie::CoercionError.new(:age, 'bad', Integer, 'invalid number')
|
|
7
|
+
# error.message # => 'cannot coerce property `:age` from `String` to `Integer`: invalid number'
|
|
2
8
|
class CoercionError < StandardError
|
|
9
|
+
# @example Include the key, source class, and destination in the message
|
|
10
|
+
# require 'hashie/extensions/coercion'
|
|
11
|
+
# error = Hashie::CoercionError.new(:age, 'bad', Integer, 'invalid number')
|
|
12
|
+
# error.message # => 'cannot coerce property `:age` from `String` to `Integer`: invalid number'
|
|
3
13
|
def initialize(key, value, into, message)
|
|
4
|
-
super("
|
|
14
|
+
super("cannot coerce property `#{key.inspect}` from `#{value.class}` to `#{into}`: #{message}")
|
|
5
15
|
end
|
|
6
16
|
end
|
|
7
17
|
|
|
8
18
|
module Extensions
|
|
19
|
+
# @example Coerce assigned values
|
|
20
|
+
# class Person < Hash
|
|
21
|
+
# include Hashie::Extensions::Coercion
|
|
22
|
+
# coerce_key :age, Integer
|
|
23
|
+
# end
|
|
24
|
+
# person = Person.new
|
|
25
|
+
# person[:age] = '42'
|
|
26
|
+
# person[:age] # => 42
|
|
9
27
|
module Coercion
|
|
10
28
|
CORE_TYPES = {
|
|
11
|
-
Integer
|
|
12
|
-
Float
|
|
13
|
-
Complex
|
|
14
|
-
Rational
|
|
15
|
-
String
|
|
16
|
-
Symbol
|
|
29
|
+
Integer => :to_i,
|
|
30
|
+
Float => :to_f,
|
|
31
|
+
Complex => :to_c,
|
|
32
|
+
Rational => :to_r,
|
|
33
|
+
String => :to_s,
|
|
34
|
+
Symbol => :to_sym
|
|
17
35
|
}.freeze
|
|
18
36
|
|
|
19
37
|
ABSTRACT_CORE_TYPES =
|
|
20
|
-
|
|
21
|
-
{ Numeric => [Integer, Float, Complex, Rational] }
|
|
22
|
-
else
|
|
23
|
-
{
|
|
24
|
-
Integer => [Fixnum, Bignum],
|
|
25
|
-
Numeric => [Fixnum, Bignum, Float, Complex, Rational]
|
|
26
|
-
}
|
|
27
|
-
end
|
|
38
|
+
{ Numeric => [Integer, Float, Complex, Rational] }.freeze
|
|
28
39
|
|
|
29
40
|
def self.included(base)
|
|
30
41
|
base.send :include, InstanceMethods
|
|
31
42
|
base.extend ClassMethods
|
|
32
|
-
unless base.method_defined?(:set_value_without_coercion)
|
|
33
|
-
base.send :alias_method, :set_value_without_coercion, :[]=
|
|
34
|
-
end
|
|
43
|
+
base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
|
|
35
44
|
base.send :alias_method, :[]=, :set_value_with_coercion
|
|
36
45
|
end
|
|
37
46
|
|
|
47
|
+
# @example Add coercing instance writers through the extension
|
|
48
|
+
# class Record < Hash
|
|
49
|
+
# include Hashie::Extensions::Coercion
|
|
50
|
+
# coerce_key :count, Integer
|
|
51
|
+
# end
|
|
52
|
+
# record = Record.new
|
|
53
|
+
# record[:count] = '3'
|
|
54
|
+
# record[:count] # => 3
|
|
38
55
|
module InstanceMethods
|
|
56
|
+
# @example Write a coerced value
|
|
57
|
+
# class Record < Hash
|
|
58
|
+
# include Hashie::Extensions::Coercion
|
|
59
|
+
# coerce_key :count, Integer
|
|
60
|
+
# end
|
|
61
|
+
# record = Record.new
|
|
62
|
+
# record.set_value_with_coercion(:count, '3')
|
|
63
|
+
# record[:count] # => 3
|
|
39
64
|
def set_value_with_coercion(key, value)
|
|
40
65
|
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
|
41
66
|
|
|
42
67
|
unless value.nil? || into.nil?
|
|
43
68
|
begin
|
|
44
69
|
value = self.class.fetch_coercion(into).call(value)
|
|
45
|
-
rescue NoMethodError, TypeError => e
|
|
70
|
+
rescue NoMethodError, TypeError, ArgumentError => e
|
|
46
71
|
raise CoercionError.new(key, value, into, e.message)
|
|
47
72
|
end
|
|
48
73
|
end
|
|
@@ -50,10 +75,46 @@ module Hashie
|
|
|
50
75
|
set_value_without_coercion(key, value)
|
|
51
76
|
end
|
|
52
77
|
|
|
78
|
+
# @example Route a custom write through coercion
|
|
79
|
+
# class Record < Hash
|
|
80
|
+
# include Hashie::Extensions::Coercion
|
|
81
|
+
# coerce_key :count, Integer
|
|
82
|
+
# end
|
|
83
|
+
# record = Record.new
|
|
84
|
+
# record.custom_writer(:count, '3')
|
|
85
|
+
# record[:count] # => 3
|
|
53
86
|
def custom_writer(key, value, _convert = true)
|
|
54
87
|
self[key] = value
|
|
55
88
|
end
|
|
56
89
|
|
|
90
|
+
# Ensures that values are coerced before Mash's own recursive
|
|
91
|
+
# Hash-to-Mash conversion has a chance to run, since by that point
|
|
92
|
+
# the value would already be an instance of `self.class` rather
|
|
93
|
+
# than a plain Hash, and value coercion would never trigger. See
|
|
94
|
+
# https://github.com/hashie/hashie/issues/265.
|
|
95
|
+
#
|
|
96
|
+
# @example Convert values by their source type
|
|
97
|
+
# class Record < Hash
|
|
98
|
+
# include Hashie::Extensions::Coercion
|
|
99
|
+
# coerce_value String, Integer
|
|
100
|
+
# end
|
|
101
|
+
# Record.new.convert_value('3') # => 3
|
|
102
|
+
# Record.new.convert_value(false) # => false
|
|
103
|
+
def convert_value(val, duping = false)
|
|
104
|
+
into = self.class.value_coercion(val)
|
|
105
|
+
return self.class.fetch_coercion(into).call(val) if into
|
|
106
|
+
|
|
107
|
+
defined?(super) ? super : val
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @example Remove old keys and coerce incoming values
|
|
111
|
+
# class Record < Hash
|
|
112
|
+
# include Hashie::Extensions::Coercion
|
|
113
|
+
# coerce_key :count, Integer
|
|
114
|
+
# end
|
|
115
|
+
# record = Record[:old, true]
|
|
116
|
+
# record.replace(count: '3')
|
|
117
|
+
# record # => { count: 3 }
|
|
57
118
|
def replace(other_hash)
|
|
58
119
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
59
120
|
other_hash.each { |key, value| self[key] = value }
|
|
@@ -61,6 +122,12 @@ module Hashie
|
|
|
61
122
|
end
|
|
62
123
|
end
|
|
63
124
|
|
|
125
|
+
# @example Configure coercion rules on a hash subclass
|
|
126
|
+
# class Record < Hash
|
|
127
|
+
# include Hashie::Extensions::Coercion
|
|
128
|
+
# coerce_key :count, Integer
|
|
129
|
+
# end
|
|
130
|
+
# Record.key_coercion(:count) # => Integer
|
|
64
131
|
module ClassMethods
|
|
65
132
|
attr_writer :key_coercions
|
|
66
133
|
protected :key_coercions=
|
|
@@ -69,30 +136,58 @@ module Hashie
|
|
|
69
136
|
# key is set it will be coerced into the specified class.
|
|
70
137
|
# Coercion will occur by first attempting to call Class.coerce
|
|
71
138
|
# and then by calling Class.new with the value as an argument
|
|
72
|
-
# in either case.
|
|
139
|
+
# in either case. Procs and other objects implementing #call
|
|
140
|
+
# may also be used.
|
|
73
141
|
#
|
|
74
|
-
# @param [Object]
|
|
75
|
-
# @param [Class] into the class into which you want the key(s) coerced.
|
|
142
|
+
# @param [Array<Object>] attrs the keys followed by the class or callable used to coerce them.
|
|
76
143
|
#
|
|
77
144
|
# @example Coerce a "user" subhash into a User object
|
|
145
|
+
# User = Struct.new(:name) do
|
|
146
|
+
# def self.coerce(attributes)
|
|
147
|
+
# new(attributes[:name])
|
|
148
|
+
# end
|
|
149
|
+
# end
|
|
78
150
|
# class Tweet < Hash
|
|
79
151
|
# include Hashie::Extensions::Coercion
|
|
80
152
|
# coerce_key :user, User
|
|
81
153
|
# end
|
|
154
|
+
# tweet = Tweet.new
|
|
155
|
+
# tweet[:user] = { name: 'Ada' }
|
|
156
|
+
# tweet[:user].name # => 'Ada'
|
|
82
157
|
def coerce_key(*attrs)
|
|
83
158
|
into = attrs.pop
|
|
84
159
|
attrs.each { |key| key_coercions[key] = into }
|
|
85
160
|
end
|
|
86
161
|
|
|
162
|
+
# @example Configure multiple keys with one rule
|
|
163
|
+
# class Record < Hash
|
|
164
|
+
# include Hashie::Extensions::Coercion
|
|
165
|
+
# coerce_keys :count, :total, Integer
|
|
166
|
+
# end
|
|
167
|
+
# record = Record.new
|
|
168
|
+
# record[:total] = '12'
|
|
169
|
+
# record[:total] # => 12
|
|
170
|
+
# Record.key_coercion(:count) # => Integer
|
|
87
171
|
alias coerce_keys coerce_key
|
|
88
172
|
|
|
89
173
|
# Returns a hash of any existing key coercions.
|
|
174
|
+
#
|
|
175
|
+
# @example Inspect configured rules
|
|
176
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
177
|
+
# klass.coerce_key(:age, Integer)
|
|
178
|
+
# klass.key_coercions # => { age: Integer }
|
|
90
179
|
def key_coercions
|
|
91
180
|
@key_coercions ||= {}
|
|
92
181
|
end
|
|
93
182
|
|
|
94
183
|
# Returns the specific key coercion for the specified key,
|
|
95
184
|
# if one exists.
|
|
185
|
+
#
|
|
186
|
+
# @example Look up symbol and string key names
|
|
187
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
188
|
+
# klass.coerce_key(:age, Integer)
|
|
189
|
+
# klass.key_coercion('age') # => Integer
|
|
190
|
+
# klass.key_coercion(:missing) # => nil
|
|
96
191
|
def key_coercion(key)
|
|
97
192
|
key_coercions[key.to_sym]
|
|
98
193
|
end
|
|
@@ -102,7 +197,7 @@ module Hashie
|
|
|
102
197
|
# class.
|
|
103
198
|
#
|
|
104
199
|
# @param [Class] from the type you would like coerced.
|
|
105
|
-
# @param [Class] into the class
|
|
200
|
+
# @param [Class, #call] into the class or callable used to coerce the value.
|
|
106
201
|
# @option options [Boolean] :strict (true) whether use exact source class
|
|
107
202
|
# only or include ancestors
|
|
108
203
|
#
|
|
@@ -118,6 +213,10 @@ module Hashie
|
|
|
118
213
|
# end
|
|
119
214
|
# end
|
|
120
215
|
# end
|
|
216
|
+
# record = SpecialHash.new
|
|
217
|
+
# record[:user] = { name: 'Ada' }
|
|
218
|
+
# record[:user].class # => SpecialHash
|
|
219
|
+
# record[:user][:name] # => 'Ada'
|
|
121
220
|
def coerce_value(from, into, options = {})
|
|
122
221
|
options = { strict: true }.merge(options)
|
|
123
222
|
|
|
@@ -138,35 +237,67 @@ module Hashie
|
|
|
138
237
|
end
|
|
139
238
|
|
|
140
239
|
# Return all value coercions that have the :strict rule as true.
|
|
240
|
+
#
|
|
241
|
+
# @example Inspect exact source-type rules
|
|
242
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
243
|
+
# klass.coerce_value(String, Integer)
|
|
244
|
+
# klass.strict_value_coercions # => { String => Integer }
|
|
141
245
|
def strict_value_coercions
|
|
142
246
|
@strict_value_coercions ||= {}
|
|
143
247
|
end
|
|
144
248
|
|
|
145
249
|
# Return all value coercions that have the :strict rule as false.
|
|
250
|
+
#
|
|
251
|
+
# @example Inspect non-strict source-type rules
|
|
252
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
253
|
+
# klass.coerce_value(Integer, String, strict: false)
|
|
254
|
+
# klass.lenient_value_coercions # => { Integer => String }
|
|
146
255
|
def lenient_value_coercions
|
|
147
256
|
@lenient_value_coercions ||= {}
|
|
148
257
|
end
|
|
149
258
|
|
|
150
259
|
# Fetch the value coercion, if any, for the specified object.
|
|
260
|
+
#
|
|
261
|
+
# @example Look up a rule from an actual value
|
|
262
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
263
|
+
# klass.coerce_value(String, Integer)
|
|
264
|
+
# klass.value_coercion('3') # => Integer
|
|
265
|
+
# klass.value_coercion(false) # => nil
|
|
151
266
|
def value_coercion(value)
|
|
152
267
|
from = value.class
|
|
153
268
|
strict_value_coercions[from] || lenient_value_coercions[from]
|
|
154
269
|
end
|
|
155
270
|
|
|
271
|
+
# @example Retrieve a conversion callable
|
|
272
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
273
|
+
# klass.fetch_coercion(Integer).call('3') # => 3
|
|
274
|
+
# klass.fetch_coercion(->(value) { value.upcase }).call('ada') # => 'ADA'
|
|
156
275
|
def fetch_coercion(type)
|
|
157
276
|
return type if type.is_a? Proc
|
|
277
|
+
return type if type.respond_to?(:call) && !type.is_a?(Module) && !type.is_a?(Enumerable)
|
|
278
|
+
|
|
158
279
|
coercion_cache[type]
|
|
159
280
|
end
|
|
160
281
|
|
|
282
|
+
# @example Lazily cache a working conversion
|
|
283
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
284
|
+
# conversion = klass.coercion_cache[Integer]
|
|
285
|
+
# conversion.call('3') # => 3
|
|
286
|
+
# klass.coercion_cache[Integer].equal?(conversion) # => true
|
|
161
287
|
def coercion_cache
|
|
162
288
|
@coercion_cache ||= ::Hash.new do |hash, type|
|
|
163
289
|
hash[type] = build_coercion(type)
|
|
164
290
|
end
|
|
165
291
|
end
|
|
166
292
|
|
|
293
|
+
# @example Build scalar and nested collection conversions
|
|
294
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
295
|
+
# klass.build_coercion(Integer).call('3') # => 3
|
|
296
|
+
# klass.build_coercion([Integer]).call(['3', '4']) # => [3, 4]
|
|
297
|
+
# klass.build_coercion({ Symbol => Integer }).call('age' => '42') # => { age: 42 }
|
|
167
298
|
def build_coercion(type)
|
|
168
299
|
if type.is_a? Enumerable
|
|
169
|
-
if type.
|
|
300
|
+
if type.instance_of?(::Hash)
|
|
170
301
|
type, key_type, value_type = type.class, *type.first
|
|
171
302
|
build_hash_coercion(type, key_type, value_type)
|
|
172
303
|
else
|
|
@@ -179,18 +310,24 @@ module Hashie
|
|
|
179
310
|
elsif type.respond_to? :coerce
|
|
180
311
|
lambda do |value|
|
|
181
312
|
return value if value.is_a? type
|
|
313
|
+
|
|
182
314
|
type.coerce(value)
|
|
183
315
|
end
|
|
184
316
|
elsif type.respond_to? :new
|
|
185
317
|
lambda do |value|
|
|
186
318
|
return value if value.is_a? type
|
|
319
|
+
|
|
187
320
|
type.new(value)
|
|
188
321
|
end
|
|
189
322
|
else
|
|
190
|
-
raise TypeError, "
|
|
323
|
+
raise TypeError, "no coercion is available for `#{type}`"
|
|
191
324
|
end
|
|
192
325
|
end
|
|
193
326
|
|
|
327
|
+
# @example Convert both hash keys and values
|
|
328
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
329
|
+
# conversion = klass.build_hash_coercion(Hash, Symbol, Integer)
|
|
330
|
+
# conversion.call('age' => '42') # => { age: 42 }
|
|
194
331
|
def build_hash_coercion(type, key_type, value_type)
|
|
195
332
|
key_coerce = fetch_coercion(key_type)
|
|
196
333
|
value_coerce = fetch_coercion(value_type)
|
|
@@ -199,6 +336,10 @@ module Hashie
|
|
|
199
336
|
end
|
|
200
337
|
end
|
|
201
338
|
|
|
339
|
+
# @example Convert each array element
|
|
340
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
341
|
+
# conversion = klass.build_container_coercion(Array, Integer)
|
|
342
|
+
# conversion.call(['3', '4']) # => [3, 4]
|
|
202
343
|
def build_container_coercion(type, value_type)
|
|
203
344
|
value_coerce = fetch_coercion(value_type)
|
|
204
345
|
lambda do |value|
|
|
@@ -206,14 +347,27 @@ module Hashie
|
|
|
206
347
|
end
|
|
207
348
|
end
|
|
208
349
|
|
|
350
|
+
# @example Use the lenient core conversion
|
|
351
|
+
# klass = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
352
|
+
# conversion = klass.build_core_type_coercion(Integer)
|
|
353
|
+
# conversion.call('12 items') # => 12
|
|
354
|
+
# conversion.call('not a number') # => 0
|
|
209
355
|
def build_core_type_coercion(type)
|
|
210
356
|
name = CORE_TYPES[type]
|
|
211
357
|
lambda do |value|
|
|
212
358
|
return value if value.is_a? type
|
|
213
|
-
|
|
359
|
+
|
|
360
|
+
value.send(name)
|
|
214
361
|
end
|
|
215
362
|
end
|
|
216
363
|
|
|
364
|
+
# @example Copy key rules into subclasses without sharing their rule hashes
|
|
365
|
+
# parent = Class.new(Hash) { include Hashie::Extensions::Coercion }
|
|
366
|
+
# parent.coerce_key(:age, Integer)
|
|
367
|
+
# child = Class.new(parent)
|
|
368
|
+
# child.coerce_key(:name, String)
|
|
369
|
+
# child.key_coercion(:age) # => Integer
|
|
370
|
+
# parent.key_coercion(:name) # => nil
|
|
217
371
|
def inherited(klass)
|
|
218
372
|
super
|
|
219
373
|
|
|
@@ -1,19 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Dash
|
|
6
|
+
# Adds per-property coercion to a Dash.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Dash) do
|
|
10
|
+
# include Hashie::Extensions::Dash::Coercion
|
|
11
|
+
# property :count, coerce: Integer
|
|
12
|
+
# end
|
|
13
|
+
# klass.new(count: '3').count # => 3
|
|
4
14
|
module Coercion
|
|
5
15
|
# Extends a Dash with the ability to define coercion for properties.
|
|
6
|
-
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# klass = Class.new(Hashie::Dash) do
|
|
19
|
+
# include Hashie::Extensions::Dash::Coercion
|
|
20
|
+
# property :count, coerce: Integer
|
|
21
|
+
# end
|
|
22
|
+
# klass.new(count: '3').count # => 3
|
|
7
23
|
def self.included(base)
|
|
8
24
|
base.send :include, Hashie::Extensions::Coercion
|
|
9
25
|
base.extend ClassMethods
|
|
10
26
|
end
|
|
11
27
|
|
|
28
|
+
# Property declarations supporting coercion.
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# klass = Class.new(Hashie::Dash) do
|
|
32
|
+
# include Hashie::Extensions::Dash::Coercion
|
|
33
|
+
# property :count, coerce: Integer
|
|
34
|
+
# end
|
|
35
|
+
# klass.new(count: '3').count # => 3
|
|
12
36
|
module ClassMethods
|
|
13
37
|
# Defines a property on the Dash. Options are the standard
|
|
14
|
-
#
|
|
38
|
+
# `Hashie::Dash#property` options plus:
|
|
39
|
+
#
|
|
40
|
+
# * `:coerce` - The class into which you want the property coerced.
|
|
15
41
|
#
|
|
16
|
-
#
|
|
42
|
+
# @example
|
|
43
|
+
# klass = Class.new(Hashie::Dash) do
|
|
44
|
+
# include Hashie::Extensions::Dash::Coercion
|
|
45
|
+
# property :count, coerce: Integer
|
|
46
|
+
# end
|
|
47
|
+
# klass.new(count: '3').count # => 3
|
|
17
48
|
def property(property_name, options = {})
|
|
18
49
|
super
|
|
19
50
|
coerce_key property_name, options[:coerce] if options[:coerce]
|
|
@@ -1,12 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Hashie
|
|
2
4
|
module Extensions
|
|
3
5
|
module Dash
|
|
6
|
+
# Allows string and symbol access to declared Dash properties.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# klass = Class.new(Hashie::Dash) do
|
|
10
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
11
|
+
# property :name
|
|
12
|
+
# end
|
|
13
|
+
# klass.new(name: 'Ada')['name'] # => 'Ada'
|
|
4
14
|
module IndifferentAccess
|
|
15
|
+
# @example Include indifferent access in a Dash subclass
|
|
16
|
+
# klass = Class.new(Hashie::Dash) do
|
|
17
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
18
|
+
# property :name
|
|
19
|
+
# end
|
|
20
|
+
# klass.property?('name') # => true
|
|
5
21
|
def self.included(base)
|
|
6
22
|
base.extend ClassMethods
|
|
7
23
|
base.send :include, Hashie::Extensions::IndifferentAccess
|
|
8
24
|
end
|
|
9
25
|
|
|
26
|
+
# @example Add Dash-specific class methods when needed
|
|
27
|
+
# klass = Class.new(Hashie::Dash) { property :name }
|
|
28
|
+
# Hashie::Extensions::Dash::IndifferentAccess.maybe_extend(klass)
|
|
29
|
+
# klass.property?('name') # => true
|
|
30
|
+
# klass.singleton_class.included_modules.include?(Hashie::Extensions::Dash::IndifferentAccess::ClassMethods) # => true
|
|
10
31
|
def self.maybe_extend(base)
|
|
11
32
|
return unless requires_class_methods?(base)
|
|
12
33
|
|
|
@@ -19,34 +40,91 @@ module Hashie
|
|
|
19
40
|
end
|
|
20
41
|
private_class_method :requires_class_methods?
|
|
21
42
|
|
|
43
|
+
# @example Include defaults under string keys in the returned hash
|
|
44
|
+
# klass = Class.new(Hashie::Dash) do
|
|
45
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
46
|
+
# property :name, default: 'Ada'
|
|
47
|
+
# property :age
|
|
48
|
+
# end
|
|
49
|
+
# klass.new(age: 37).to_h # => {'name' => 'Ada', 'age' => 37}
|
|
22
50
|
def to_h
|
|
23
|
-
defaults =
|
|
51
|
+
defaults = self.class.properties.to_h do |prop|
|
|
24
52
|
[Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]]
|
|
25
|
-
end
|
|
53
|
+
end
|
|
26
54
|
|
|
27
55
|
defaults.merge(self)
|
|
28
56
|
end
|
|
57
|
+
# @example
|
|
58
|
+
# klass = Class.new(Hashie::Dash) do
|
|
59
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
60
|
+
# property :name, default: 'Ada'
|
|
61
|
+
# end
|
|
62
|
+
# klass.new.to_hash # => {'name' => 'Ada'}
|
|
29
63
|
alias to_hash to_h
|
|
30
64
|
|
|
65
|
+
# Class-level property and translation lookup with indifferent keys.
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# klass = Class.new(Hashie::Dash) do
|
|
69
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
70
|
+
# property :name
|
|
71
|
+
# end
|
|
72
|
+
# klass.property?('name') # => true
|
|
31
73
|
module ClassMethods
|
|
32
74
|
# Check to see if the specified property has already been
|
|
33
75
|
# defined.
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# klass = Class.new(Hashie::Dash) do
|
|
79
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
80
|
+
# property :name
|
|
81
|
+
# end
|
|
82
|
+
# klass.property?('name') # => true
|
|
83
|
+
# klass.property?(:missing) # => false
|
|
34
84
|
def property?(name)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
85
|
+
targets = if translation_for?(name)
|
|
86
|
+
Array(translations.find { |key, _| key.to_s == name.to_s }.last)
|
|
87
|
+
else
|
|
88
|
+
[name]
|
|
89
|
+
end
|
|
90
|
+
targets.all? do |target|
|
|
91
|
+
properties.any? { |property| property.to_s == target.to_s }
|
|
92
|
+
end
|
|
38
93
|
end
|
|
39
94
|
|
|
95
|
+
# @example Look up a translation with a string or symbol
|
|
96
|
+
# klass = Class.new(Hashie::Dash) do
|
|
97
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
98
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
99
|
+
# property :name, from: :fullName
|
|
100
|
+
# end
|
|
101
|
+
# klass.translation_exists?('fullName') # => true
|
|
102
|
+
# klass.translation_exists?(:missing) # => false
|
|
40
103
|
def translation_exists?(name)
|
|
41
104
|
name = name.to_s
|
|
42
105
|
!!translations.keys.find { |key| key.to_s == name }
|
|
43
106
|
end
|
|
44
107
|
|
|
108
|
+
# @example Apply a transformer declared with a symbol key
|
|
109
|
+
# klass = Class.new(Hashie::Dash) do
|
|
110
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
111
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
112
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
113
|
+
# end
|
|
114
|
+
# klass.transformed_property('age', '37') # => 37
|
|
45
115
|
def transformed_property(property_name, value)
|
|
46
116
|
transform = transforms[property_name] || transforms[property_name.to_sym]
|
|
47
117
|
transform.call(value)
|
|
48
118
|
end
|
|
49
119
|
|
|
120
|
+
# @example Find a transformer regardless of key type
|
|
121
|
+
# klass = Class.new(Hashie::Dash) do
|
|
122
|
+
# include Hashie::Extensions::Dash::PropertyTranslation
|
|
123
|
+
# include Hashie::Extensions::Dash::IndifferentAccess
|
|
124
|
+
# property :age, transform_with: ->(value) { value.to_i }
|
|
125
|
+
# end
|
|
126
|
+
# klass.transformation_exists?('age') # => true
|
|
127
|
+
# klass.transformation_exists?(:missing) # => false
|
|
50
128
|
def transformation_exists?(name)
|
|
51
129
|
name = name.to_s
|
|
52
130
|
!!transforms.keys.find { |key| key.to_s == name }
|