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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +56 -0
  3. data/CONTRIBUTING.md +38 -1
  4. data/README.md +816 -181
  5. data/Rakefile +29 -1
  6. data/UPGRADING.md +18 -0
  7. data/hashie.gemspec +8 -7
  8. data/lib/hashie/array.rb +15 -12
  9. data/lib/hashie/clash.rb +59 -4
  10. data/lib/hashie/dash.rb +144 -16
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +38 -0
  12. data/lib/hashie/extensions/array/pretty_inspect.rb +20 -0
  13. data/lib/hashie/extensions/block_access.rb +71 -0
  14. data/lib/hashie/extensions/coercion.rb +180 -19
  15. data/lib/hashie/extensions/dash/coercion.rb +34 -3
  16. data/lib/hashie/extensions/dash/indifferent_access.rb +83 -5
  17. data/lib/hashie/extensions/dash/lazy.rb +170 -0
  18. data/lib/hashie/extensions/dash/predefined_values.rb +69 -9
  19. data/lib/hashie/extensions/dash/property_translation.rb +134 -16
  20. data/lib/hashie/extensions/deep_fetch.rb +21 -9
  21. data/lib/hashie/extensions/deep_find.rb +20 -0
  22. data/lib/hashie/extensions/deep_grep.rb +69 -0
  23. data/lib/hashie/extensions/deep_locate.rb +15 -28
  24. data/lib/hashie/extensions/deep_merge.rb +16 -0
  25. data/lib/hashie/extensions/deep_merge_initializer.rb +42 -0
  26. data/lib/hashie/extensions/ignore_undeclared.rb +21 -3
  27. data/lib/hashie/extensions/indifferent_access.rb +100 -14
  28. data/lib/hashie/extensions/kernel_coercion.rb +58 -0
  29. data/lib/hashie/extensions/key_conflict_warning.rb +39 -4
  30. data/lib/hashie/extensions/key_conversion.rb +9 -0
  31. data/lib/hashie/extensions/mash/define_accessors.rb +71 -8
  32. data/lib/hashie/extensions/mash/keep_original_keys.rb +13 -9
  33. data/lib/hashie/extensions/mash/permissive_respond_to.rb +2 -0
  34. data/lib/hashie/extensions/mash/required_keys.rb +111 -0
  35. data/lib/hashie/extensions/mash/safe_assignment.rb +22 -4
  36. data/lib/hashie/extensions/mash/strict_key_access.rb +65 -0
  37. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -2
  38. data/lib/hashie/extensions/mash/transform_keys.rb +86 -0
  39. data/lib/hashie/extensions/mash/underscore_keys.rb +32 -0
  40. data/lib/hashie/extensions/merge_initializer.rb +9 -0
  41. data/lib/hashie/extensions/method_access.rb +93 -7
  42. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -35
  43. data/lib/hashie/extensions/parsers/yaml_parser.rb +81 -0
  44. data/lib/hashie/extensions/persistable.rb +137 -0
  45. data/lib/hashie/extensions/pretty_inspect.rb +14 -0
  46. data/lib/hashie/extensions/ruby_version.rb +18 -1
  47. data/lib/hashie/extensions/ruby_version_check.rb +17 -0
  48. data/lib/hashie/extensions/strict_key_access.rb +59 -5
  49. data/lib/hashie/extensions/stringify_keys.rb +72 -17
  50. data/lib/hashie/extensions/symbolize_keys.rb +72 -16
  51. data/lib/hashie/hash.rb +16 -0
  52. data/lib/hashie/logger.rb +18 -1
  53. data/lib/hashie/mash.rb +293 -62
  54. data/lib/hashie/railtie.rb +27 -1
  55. data/lib/hashie/rash.rb +64 -4
  56. data/lib/hashie/trash.rb +8 -0
  57. data/lib/hashie/utils.rb +17 -6
  58. data/lib/hashie/version.rb +3 -1
  59. data/lib/hashie.rb +45 -0
  60. 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("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
- { 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] key the key or array of keys you would like to be coerced.
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 into which you would like the value coerced.
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.class == ::Hash
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, "#{type} is not a coercable type"
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
- return value.send(name)
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
- # <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]
@@ -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 = ::Hash[self.class.properties.map do |prop|
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
- name = translations[name.to_sym] if translation_for?(name)
36
- name = name.to_s
37
- !!properties.find { |property| property.to_s == name }
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 }