hashie 5.1.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 +56 -0
- data/CONTRIBUTING.md +38 -1
- data/README.md +816 -181
- data/Rakefile +29 -1
- data/UPGRADING.md +18 -0
- data/hashie.gemspec +8 -7
- data/lib/hashie/array.rb +15 -12
- data/lib/hashie/clash.rb +59 -4
- data/lib/hashie/dash.rb +144 -16
- 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 -19
- 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 +15 -28
- 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 +93 -7
- 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 +293 -62
- 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 +15 -17
|
@@ -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,41 +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
|
-
{ Numeric => [Integer, Float, Complex, Rational] }
|
|
38
|
+
{ Numeric => [Integer, Float, Complex, Rational] }.freeze
|
|
21
39
|
|
|
22
40
|
def self.included(base)
|
|
23
41
|
base.send :include, InstanceMethods
|
|
24
42
|
base.extend ClassMethods
|
|
25
|
-
unless base.method_defined?(:set_value_without_coercion)
|
|
26
|
-
base.send :alias_method, :set_value_without_coercion, :[]=
|
|
27
|
-
end
|
|
43
|
+
base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
|
|
28
44
|
base.send :alias_method, :[]=, :set_value_with_coercion
|
|
29
45
|
end
|
|
30
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
|
|
31
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
|
|
32
64
|
def set_value_with_coercion(key, value)
|
|
33
65
|
into = self.class.key_coercion(key) || self.class.value_coercion(value)
|
|
34
66
|
|
|
35
67
|
unless value.nil? || into.nil?
|
|
36
68
|
begin
|
|
37
69
|
value = self.class.fetch_coercion(into).call(value)
|
|
38
|
-
rescue NoMethodError, TypeError => e
|
|
70
|
+
rescue NoMethodError, TypeError, ArgumentError => e
|
|
39
71
|
raise CoercionError.new(key, value, into, e.message)
|
|
40
72
|
end
|
|
41
73
|
end
|
|
@@ -43,10 +75,46 @@ module Hashie
|
|
|
43
75
|
set_value_without_coercion(key, value)
|
|
44
76
|
end
|
|
45
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
|
|
46
86
|
def custom_writer(key, value, _convert = true)
|
|
47
87
|
self[key] = value
|
|
48
88
|
end
|
|
49
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 }
|
|
50
118
|
def replace(other_hash)
|
|
51
119
|
(keys - other_hash.keys).each { |key| delete(key) }
|
|
52
120
|
other_hash.each { |key, value| self[key] = value }
|
|
@@ -54,6 +122,12 @@ module Hashie
|
|
|
54
122
|
end
|
|
55
123
|
end
|
|
56
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
|
|
57
131
|
module ClassMethods
|
|
58
132
|
attr_writer :key_coercions
|
|
59
133
|
protected :key_coercions=
|
|
@@ -62,30 +136,58 @@ module Hashie
|
|
|
62
136
|
# key is set it will be coerced into the specified class.
|
|
63
137
|
# Coercion will occur by first attempting to call Class.coerce
|
|
64
138
|
# and then by calling Class.new with the value as an argument
|
|
65
|
-
# in either case.
|
|
139
|
+
# in either case. Procs and other objects implementing #call
|
|
140
|
+
# may also be used.
|
|
66
141
|
#
|
|
67
|
-
# @param [Object]
|
|
68
|
-
# @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.
|
|
69
143
|
#
|
|
70
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
|
|
71
150
|
# class Tweet < Hash
|
|
72
151
|
# include Hashie::Extensions::Coercion
|
|
73
152
|
# coerce_key :user, User
|
|
74
153
|
# end
|
|
154
|
+
# tweet = Tweet.new
|
|
155
|
+
# tweet[:user] = { name: 'Ada' }
|
|
156
|
+
# tweet[:user].name # => 'Ada'
|
|
75
157
|
def coerce_key(*attrs)
|
|
76
158
|
into = attrs.pop
|
|
77
159
|
attrs.each { |key| key_coercions[key] = into }
|
|
78
160
|
end
|
|
79
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
|
|
80
171
|
alias coerce_keys coerce_key
|
|
81
172
|
|
|
82
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 }
|
|
83
179
|
def key_coercions
|
|
84
180
|
@key_coercions ||= {}
|
|
85
181
|
end
|
|
86
182
|
|
|
87
183
|
# Returns the specific key coercion for the specified key,
|
|
88
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
|
|
89
191
|
def key_coercion(key)
|
|
90
192
|
key_coercions[key.to_sym]
|
|
91
193
|
end
|
|
@@ -95,7 +197,7 @@ module Hashie
|
|
|
95
197
|
# class.
|
|
96
198
|
#
|
|
97
199
|
# @param [Class] from the type you would like coerced.
|
|
98
|
-
# @param [Class] into the class
|
|
200
|
+
# @param [Class, #call] into the class or callable used to coerce the value.
|
|
99
201
|
# @option options [Boolean] :strict (true) whether use exact source class
|
|
100
202
|
# only or include ancestors
|
|
101
203
|
#
|
|
@@ -111,6 +213,10 @@ module Hashie
|
|
|
111
213
|
# end
|
|
112
214
|
# end
|
|
113
215
|
# end
|
|
216
|
+
# record = SpecialHash.new
|
|
217
|
+
# record[:user] = { name: 'Ada' }
|
|
218
|
+
# record[:user].class # => SpecialHash
|
|
219
|
+
# record[:user][:name] # => 'Ada'
|
|
114
220
|
def coerce_value(from, into, options = {})
|
|
115
221
|
options = { strict: true }.merge(options)
|
|
116
222
|
|
|
@@ -131,35 +237,67 @@ module Hashie
|
|
|
131
237
|
end
|
|
132
238
|
|
|
133
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 }
|
|
134
245
|
def strict_value_coercions
|
|
135
246
|
@strict_value_coercions ||= {}
|
|
136
247
|
end
|
|
137
248
|
|
|
138
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 }
|
|
139
255
|
def lenient_value_coercions
|
|
140
256
|
@lenient_value_coercions ||= {}
|
|
141
257
|
end
|
|
142
258
|
|
|
143
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
|
|
144
266
|
def value_coercion(value)
|
|
145
267
|
from = value.class
|
|
146
268
|
strict_value_coercions[from] || lenient_value_coercions[from]
|
|
147
269
|
end
|
|
148
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'
|
|
149
275
|
def fetch_coercion(type)
|
|
150
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
|
+
|
|
151
279
|
coercion_cache[type]
|
|
152
280
|
end
|
|
153
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
|
|
154
287
|
def coercion_cache
|
|
155
288
|
@coercion_cache ||= ::Hash.new do |hash, type|
|
|
156
289
|
hash[type] = build_coercion(type)
|
|
157
290
|
end
|
|
158
291
|
end
|
|
159
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 }
|
|
160
298
|
def build_coercion(type)
|
|
161
299
|
if type.is_a? Enumerable
|
|
162
|
-
if type.
|
|
300
|
+
if type.instance_of?(::Hash)
|
|
163
301
|
type, key_type, value_type = type.class, *type.first
|
|
164
302
|
build_hash_coercion(type, key_type, value_type)
|
|
165
303
|
else
|
|
@@ -172,18 +310,24 @@ module Hashie
|
|
|
172
310
|
elsif type.respond_to? :coerce
|
|
173
311
|
lambda do |value|
|
|
174
312
|
return value if value.is_a? type
|
|
313
|
+
|
|
175
314
|
type.coerce(value)
|
|
176
315
|
end
|
|
177
316
|
elsif type.respond_to? :new
|
|
178
317
|
lambda do |value|
|
|
179
318
|
return value if value.is_a? type
|
|
319
|
+
|
|
180
320
|
type.new(value)
|
|
181
321
|
end
|
|
182
322
|
else
|
|
183
|
-
raise TypeError, "
|
|
323
|
+
raise TypeError, "no coercion is available for `#{type}`"
|
|
184
324
|
end
|
|
185
325
|
end
|
|
186
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 }
|
|
187
331
|
def build_hash_coercion(type, key_type, value_type)
|
|
188
332
|
key_coerce = fetch_coercion(key_type)
|
|
189
333
|
value_coerce = fetch_coercion(value_type)
|
|
@@ -192,6 +336,10 @@ module Hashie
|
|
|
192
336
|
end
|
|
193
337
|
end
|
|
194
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]
|
|
195
343
|
def build_container_coercion(type, value_type)
|
|
196
344
|
value_coerce = fetch_coercion(value_type)
|
|
197
345
|
lambda do |value|
|
|
@@ -199,14 +347,27 @@ module Hashie
|
|
|
199
347
|
end
|
|
200
348
|
end
|
|
201
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
|
|
202
355
|
def build_core_type_coercion(type)
|
|
203
356
|
name = CORE_TYPES[type]
|
|
204
357
|
lambda do |value|
|
|
205
358
|
return value if value.is_a? type
|
|
206
|
-
|
|
359
|
+
|
|
360
|
+
value.send(name)
|
|
207
361
|
end
|
|
208
362
|
end
|
|
209
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
|
|
210
371
|
def inherited(klass)
|
|
211
372
|
super
|
|
212
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 }
|