hashie 4.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -53
  3. data/CONTRIBUTING.md +38 -1
  4. data/LICENSE +1 -1
  5. data/README.md +920 -205
  6. data/Rakefile +32 -1
  7. data/UPGRADING.md +56 -0
  8. data/hashie.gemspec +10 -5
  9. data/lib/hashie/array.rb +15 -12
  10. data/lib/hashie/clash.rb +59 -4
  11. data/lib/hashie/dash.rb +160 -32
  12. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  13. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  14. data/lib/hashie/extensions/block_access.rb +71 -0
  15. data/lib/hashie/extensions/coercion.rb +180 -26
  16. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  17. data/lib/hashie/extensions/dash/indifferent_access.rb +90 -3
  18. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  19. data/lib/hashie/extensions/dash/predefined_values.rb +148 -0
  20. data/lib/hashie/extensions/dash/property_translation.rb +138 -15
  21. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  22. data/lib/hashie/extensions/deep_find.rb +20 -0
  23. data/lib/hashie/extensions/deep_grep.rb +69 -0
  24. data/lib/hashie/extensions/deep_locate.rb +16 -29
  25. data/lib/hashie/extensions/deep_merge.rb +16 -0
  26. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  27. data/lib/hashie/extensions/ignore_undeclared.rb +24 -7
  28. data/lib/hashie/extensions/indifferent_access.rb +124 -5
  29. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  30. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  31. data/lib/hashie/extensions/key_conversion.rb +9 -0
  32. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  33. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  34. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  35. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  36. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  37. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  38. data/lib/hashie/extensions/mash/symbolize_keys.rb +11 -7
  39. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  40. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  41. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  42. data/lib/hashie/extensions/method_access.rb +94 -8
  43. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  44. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  45. data/lib/hashie/extensions/persistable.rb +137 -0
  46. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  47. data/lib/hashie/extensions/ruby_version.rb +18 -1
  48. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  49. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  50. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  51. data/lib/hashie/extensions/symbolize_keys.rb +84 -17
  52. data/lib/hashie/hash.rb +18 -2
  53. data/lib/hashie/logger.rb +18 -1
  54. data/lib/hashie/mash.rb +314 -82
  55. data/lib/hashie/railtie.rb +27 -1
  56. data/lib/hashie/rash.rb +64 -4
  57. data/lib/hashie/trash.rb +8 -0
  58. data/lib/hashie/utils.rb +17 -6
  59. data/lib/hashie/version.rb +3 -1
  60. data/lib/hashie.rb +46 -0
  61. metadata +22 -9
@@ -1,8 +1,46 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
5
+ # Compatibility extensions for Active Support.
6
+ #
7
+ # @example
8
+ # require 'active_support/core_ext/hash/except'
9
+ # require 'hashie/extensions/active_support/core_ext/hash'
10
+ # klass = Class.new(Hashie::Mash) do
11
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
12
+ # end
13
+ # klass.new(name: 'Ada', age: 37).except(:age).to_h # => {'name' => 'Ada'}
3
14
  module ActiveSupport
15
+ # Adapts Active Support core extensions for Hashie objects.
16
+ #
17
+ # @example
18
+ # require 'active_support/core_ext/hash/except'
19
+ # require 'hashie/extensions/active_support/core_ext/hash'
20
+ # klass = Class.new(Hashie::Mash) do
21
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
22
+ # end
23
+ # klass.new(name: 'Ada').except(:name).to_h # => {}
4
24
  module CoreExt
25
+ # Converts excluded keys using Mash's key normalization.
26
+ #
27
+ # @example
28
+ # require 'active_support/core_ext/hash/except'
29
+ # require 'hashie/extensions/active_support/core_ext/hash'
30
+ # klass = Class.new(Hashie::Mash) do
31
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
32
+ # end
33
+ # klass.new(name: 'Ada', age: 37).except(:age).to_h # => {'name' => 'Ada'}
5
34
  module Hash
35
+ # @example Exclude symbol keys from a string-keyed Mash
36
+ # require 'active_support/core_ext/hash/except'
37
+ # require 'hashie/extensions/active_support/core_ext/hash'
38
+ # klass = Class.new(Hashie::Mash) do
39
+ # include Hashie::Extensions::ActiveSupport::CoreExt::Hash
40
+ # end
41
+ # mash = klass.new(name: 'Ada', age: 37)
42
+ # mash.except(:age).to_h # => {'name' => 'Ada'}
43
+ # mash.keys # => ['name', 'age']
6
44
  def except(*keys)
7
45
  string_keys = keys.map { |key| convert_key(key) }
8
46
  super(*string_keys)
@@ -1,12 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hashie
2
4
  module Extensions
3
5
  module Array
6
+ # Formats array inspection with the concrete class name.
7
+ #
8
+ # @example
9
+ # klass = Class.new(Array) do
10
+ # include Hashie::Extensions::Array::PrettyInspect
11
+ # end
12
+ # array = klass.new([1, 'two'])
13
+ # array.inspect # => "#<#{klass} [1, \"two\"]>"
4
14
  module PrettyInspect
15
+ # @example Retain the original inspection as array_inspect
16
+ # klass = Class.new(Array) do
17
+ # include Hashie::Extensions::Array::PrettyInspect
18
+ # end
19
+ # klass.new([1, 2]).array_inspect # => '[1, 2]'
5
20
  def self.included(base)
6
21
  base.send :alias_method, :array_inspect, :inspect
7
22
  base.send :alias_method, :inspect, :hashie_inspect
8
23
  end
9
24
 
25
+ # @example
26
+ # klass = Class.new(Array) do
27
+ # include Hashie::Extensions::Array::PrettyInspect
28
+ # end
29
+ # klass.new([1, 2]).hashie_inspect # => "#<#{klass} [1, 2]>"
10
30
  def hashie_inspect
11
31
  ret = "#<#{self.class} ["
12
32
  ret << to_a.map(&:inspect).join(', ')
@@ -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("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
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 => :to_i,
12
- Float => :to_f,
13
- Complex => :to_c,
14
- Rational => :to_r,
15
- String => :to_s,
16
- Symbol => :to_sym
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
- 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
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] key the key or array of keys you would like to be coerced.
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 into which you would like the value coerced.
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.class == ::Hash
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, "#{type} is not a coercable type"
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
- return value.send(name)
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
- # <tt>Hashie::Dash#property</tt> options plus:
38
+ # `Hashie::Dash#property` options plus:
39
+ #
40
+ # * `:coerce` - The class into which you want the property coerced.
15
41
  #
16
- # * <tt>:coerce</tt> - The class into which you want the property coerced.
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]