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
data/README.md CHANGED
@@ -1,24 +1,88 @@
1
1
  # Hashie
2
2
 
3
3
  [![Join the chat at https://gitter.im/hashie/hashie](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hashie/hashie?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4
- [![Gem Version](http://img.shields.io/gem/v/hashie.svg)](http://badge.fury.io/rb/hashie)
5
- [![Build Status](http://img.shields.io/travis/hashie/hashie.svg)](https://travis-ci.org/hashie/hashie)
6
- [![Test Coverage](https://api.codeclimate.com/v1/badges/7a0b42c8a22c945571fd/test_coverage)](https://codeclimate.com/github/hashie/hashie/test_coverage)
7
- [![Maintainability](https://api.codeclimate.com/v1/badges/7a0b42c8a22c945571fd/maintainability)](https://codeclimate.com/github/hashie/hashie/maintainability)
8
-
9
- Hashie is a growing collection of tools that extend Hashes and make them more useful.
4
+ [![Gem Version](https://img.shields.io/gem/v/hashie.svg)](https://rubygems.org/gems/hashie)
5
+ [![Build Status](https://github.com/hashie/hashie/actions/workflows/test.yml/badge.svg)](https://github.com/hashie/hashie/actions/workflows/test.yml)
6
+ [![Coverage Status](https://coveralls.io/repos/github/hashie/hashie/badge.svg)](https://coveralls.io/github/hashie/hashie)
7
+
8
+ [![eierlegende Wollmilchsau](./mascot.svg)](#mascot) Hashie is a growing collection of tools that extend Hashes and make them more useful.
9
+
10
+ ## Table of Contents
11
+
12
+ - [Installation](#installation)
13
+ - [Stable Release](#stable-release)
14
+ - [Hash Extensions](#hash-extensions)
15
+ - [Coercion](#coercion)
16
+ - [Coercing Collections](#coercing-collections)
17
+ - [Coercing Hashes](#coercing-hashes)
18
+ - [Coercing Core Types](#coercing-core-types)
19
+ - [Coercion Proc](#coercion-proc)
20
+ - [Circular Coercion](#circular-coercion)
21
+ - [Nil Values and Defaults](#nil-values-and-defaults)
22
+ - [KernelCoercion](#kernelcoercion)
23
+ - [Callable Coercion](#callable-coercion)
24
+ - [Coercion with dry-types](#coercion-with-dry-types)
25
+ - [Coercion with ActiveModel](#coercion-with-activemodel)
26
+ - [KeyConversion](#keyconversion)
27
+ - [MergeInitializer](#mergeinitializer)
28
+ - [DeepMergeInitializer](#deepmergeinitializer)
29
+ - [MethodAccess](#methodaccess)
30
+ - [BlockAccess](#blockaccess)
31
+ - [Persistable](#persistable)
32
+ - [MethodAccessWithOverride](#methodaccesswithoverride)
33
+ - [MethodOverridingInitializer](#methodoverridinginitializer)
34
+ - [IndifferentAccess](#indifferentaccess)
35
+ - [IgnoreUndeclared](#ignoreundeclared)
36
+ - [DeepMerge](#deepmerge)
37
+ - [DeepFetch](#deepfetch)
38
+ - [DeepFind](#deepfind)
39
+ - [DeepGrep](#deepgrep)
40
+ - [DeepLocate](#deeplocate)
41
+ - [StrictKeyAccess](#strictkeyaccess)
42
+ - [Mash](#mash)
43
+ - [Method Conflicts](#method-conflicts)
44
+ - [Nested Hashes](#nested-hashes)
45
+ - [Key Types](#key-types)
46
+ - [Loading Files](#loading-files)
47
+ - [Mash and Rails 4 Strong Parameters](#mash-and-rails-4-strong-parameters)
48
+ - [KeepOriginalKeys](#keeporiginalkeys)
49
+ - [PermissiveRespondTo](#permissiverespondto)
50
+ - [SafeAssignment](#safeassignment)
51
+ - [TransformKeys](#transformkeys)
52
+ - [UnderscoreKeys](#underscorekeys)
53
+ - [SymbolizeKeys](#symbolizekeys)
54
+ - [DefineAccessors](#defineaccessors)
55
+ - [StrictKeyAccess](#strictkeyaccess-1)
56
+ - [RequiredKeys](#requiredkeys)
57
+ - [Dash](#dash)
58
+ - [Lazy Property Loading](#lazy-property-loading)
59
+ - [Validations with ActiveModel](#validations-with-activemodel)
60
+ - [Potential Gotchas](#potential-gotchas)
61
+ - [PropertyTranslation](#propertytranslation)
62
+ - [Coercion](#coercion-1)
63
+ - [PredefinedValues](#predefinedvalues)
64
+ - [Trash](#trash)
65
+ - [Clash](#clash)
66
+ - [Rash](#rash)
67
+ - [Auto-Optimized](#auto-optimized)
68
+ - [Logging](#logging)
69
+ - [Mascot](#mascot)
70
+ - [Contributing](#contributing)
71
+ - [Copyright](#copyright)
10
72
 
11
73
  ## Installation
12
74
 
75
+ Hashie requires Ruby 3.0 or newer.
76
+
13
77
  Hashie is available as a RubyGem:
14
78
 
15
79
  ```bash
16
- $ gem install hashie
80
+ gem install hashie
17
81
  ```
18
82
 
19
83
  ## Stable Release
20
84
 
21
- You're reading the documentation for the stable release of Hashie, 4.1.0.(https://github.com/hashie/hashie/blob/v4.1.0/README.md).
85
+ You're reading the documentation for the stable release of Hashie, 5.2.0.
22
86
 
23
87
  ## Hash Extensions
24
88
 
@@ -26,20 +90,16 @@ The library is broken up into a number of atomically includable Hash extension m
26
90
 
27
91
  Any of the extensions listed below can be mixed into a class by `include`-ing `Hashie::Extensions::ExtensionName`.
28
92
 
29
- ## Logging
30
-
31
- Hashie has a built-in logger that you can override. By default, it logs to `STDOUT` but can be replaced by any `Logger` class. The logger is accessible on the Hashie module, as shown below:
32
-
33
- ```ruby
34
- # Set the logger to the Rails logger
35
- Hashie.logger = Rails.logger
36
- ```
93
+ Hashie's extensions and classes can add method-style access to keys ([Mash](#mash)), define allowed properties with defaults and required values ([Dash](#dash)), rename or transform incoming properties ([Trash](#trash)), build nested option hashes through chained calls ([Clash](#clash)), or match keys using regular expressions and ranges ([Rash](#rash)). Use these ready-made classes or mix individual extensions into your own Hash subclass to add only the behavior you need.
37
94
 
38
95
  ### Coercion
39
96
 
40
- Coercions allow you to set up "coercion rules" based either on the key or the value type to massage data as it's being inserted into the Hash. Key coercions might be used, for example, in lightweight data modeling applications such as an API client:
97
+ Include `Hashie::Extensions::Coercion` to convert values on assignment. Use `coerce_key` to choose a conversion by key, for example when wrapping API data:
41
98
 
42
99
  ```ruby
100
+ class User < Hashie::Mash
101
+ end
102
+
43
103
  class Tweet < Hash
44
104
  include Hashie::Extensions::Coercion
45
105
  include Hashie::Extensions::MergeInitializer
@@ -47,9 +107,10 @@ class Tweet < Hash
47
107
  end
48
108
 
49
109
  user_hash = { name: "Bob" }
50
- Tweet.new(user: user_hash)
51
- # => automatically calls User.coerce(user_hash) or
52
- # User.new(user_hash) if that isn't present.
110
+ tweet = Tweet.new(user: user_hash)
111
+ # Automatically calls User.coerce(user_hash), or User.new(user_hash) if absent.
112
+ tweet[:user].class # => User
113
+ tweet[:user].name # => "Bob"
53
114
  ```
54
115
 
55
116
  Value coercions, on the other hand, will coerce values based on the type of the value being inserted. This is useful if you are trying to build a Hash-like class that is self-propagating.
@@ -66,31 +127,45 @@ class SpecialHash < Hash
66
127
  end
67
128
  end
68
129
  end
130
+
131
+ hash = SpecialHash.new(child: { name: 'Bob' })
132
+ hash[:child].class # => SpecialHash
133
+ hash[:child][:name] # => 'Bob'
69
134
  ```
70
135
 
71
- ### Coercing Collections
136
+ #### Coercing Collections
72
137
 
73
138
  ```ruby
139
+ require 'set'
140
+
141
+ class User < Hashie::Mash
142
+ end
143
+
74
144
  class Tweet < Hash
75
145
  include Hashie::Extensions::Coercion
146
+ include Hashie::Extensions::MergeInitializer
147
+ include Hashie::Extensions::MethodAccess
76
148
  coerce_key :mentions, Array[User]
77
149
  coerce_key :friends, Set[User]
78
150
  end
79
151
 
80
152
  user_hash = { name: "Bob" }
81
- mentions_hash= [user_hash, user_hash]
153
+ mentions_hash = [user_hash, user_hash]
82
154
  friends_hash = [user_hash]
83
155
  tweet = Tweet.new(mentions: mentions_hash, friends: friends_hash)
84
- # => automatically calls User.coerce(user_hash) or
85
- # User.new(user_hash) if that isn't present on each element of the array
156
+ # Automatically calls User.coerce(user_hash), or User.new(user_hash) if absent,
157
+ # on each element of the array.
86
158
 
87
159
  tweet.mentions.map(&:class) # => [User, User]
88
160
  tweet.friends.class # => Set
89
161
  ```
90
162
 
91
- ### Coercing Hashes
163
+ #### Coercing Hashes
92
164
 
93
165
  ```ruby
166
+ class User < Hashie::Mash
167
+ end
168
+
94
169
  class Relation
95
170
  def initialize(string)
96
171
  @relation = string
@@ -99,20 +174,22 @@ end
99
174
 
100
175
  class Tweet < Hash
101
176
  include Hashie::Extensions::Coercion
177
+ include Hashie::Extensions::MergeInitializer
178
+ include Hashie::Extensions::MethodAccess
102
179
  coerce_key :relations, Hash[User => Relation]
103
180
  end
104
181
 
105
182
  user_hash = { name: "Bob" }
106
- relations_hash= { user_hash => "father", user_hash => "friend" }
183
+ relations_hash = { user_hash => "father", { name: "Alice" } => "friend" }
107
184
  tweet = Tweet.new(relations: relations_hash)
108
185
  tweet.relations.map { |k,v| [k.class, v.class] } # => [[User, Relation], [User, Relation]]
109
186
  tweet.relations.class # => Hash
110
187
 
111
- # => automatically calls User.coerce(user_hash) on each key
112
- # and Relation.new on each value since Relation doesn't define the `coerce` class method
188
+ # Automatically calls User.coerce(user_hash) or User.new(user_hash) on each key
189
+ # and Relation.new on each value since Relation doesn't define `coerce`.
113
190
  ```
114
191
 
115
- ### Coercing Core Types
192
+ #### Coercing Core Types
116
193
 
117
194
  Hashie handles coercion to the following by using standard conversion methods:
118
195
 
@@ -127,19 +204,18 @@ Hashie handles coercion to the following by using standard conversion methods:
127
204
 
128
205
  **Note**: The standard Ruby conversion methods are less strict than you may assume. For example, `:foo.to_i` raises an error but `"foo".to_i` returns 0.
129
206
 
130
- You can also use coerce from the following supertypes with `coerce_value`:
131
- - Integer
132
- - Numeric
207
+ `coerce_value` also supports the supertypes `Integer` and `Numeric`.
133
208
 
134
- Hashie does not have built-in support for coercing boolean values, since Ruby does not have a built-in boolean type or standard method for coercing to a boolean. You can coerce to booleans using a custom proc.
209
+ Hashie does not choose a built-in boolean coercion policy. You can coerce to booleans using a custom proc or an external callable coercer.
135
210
 
136
- ### Coercion Proc
211
+ #### Coercion Proc
137
212
 
138
213
  You can use a custom coercion proc on either `#coerce_key` or `#coerce_value`. This is useful for coercing to booleans or other simple types without creating a new class and `coerce` method. For example:
139
214
 
140
215
  ```ruby
141
216
  class Tweet < Hash
142
217
  include Hashie::Extensions::Coercion
218
+ include Hashie::Extensions::MergeInitializer
143
219
  coerce_key :retweeted, ->(v) do
144
220
  case v
145
221
  when String
@@ -151,60 +227,251 @@ class Tweet < Hash
151
227
  end
152
228
  end
153
229
  end
230
+
231
+ Tweet.new(retweeted: 'yes')[:retweeted] # => true
232
+ Tweet.new(retweeted: 'false')[:retweeted] # => false
154
233
  ```
155
234
 
156
- #### A note on circular coercion
235
+ ##### Circular Coercion
157
236
 
158
237
  Since `coerce_key` is a class-level method, you cannot have circular coercion without the use of a proc. For example:
159
238
 
239
+ ```ruby
240
+ def define_category
241
+ Class.new(Hash) do
242
+ include Hashie::Extensions::Coercion
243
+ include Hashie::Extensions::MergeInitializer
244
+
245
+ coerce_key :products, Array[ProductHash]
246
+ end
247
+ end
248
+
249
+ begin
250
+ define_category
251
+ rescue NameError
252
+ # Expected: NameError, /ProductHash/
253
+ end
254
+ ```
255
+
256
+ This fails with a `NameError` for `ProductHash` because `ProductHash` is not defined at the point that `coerce_key` is happening.
257
+
258
+ To work around this, you can use a coercion proc. For example, you could do:
259
+
160
260
  ```ruby
161
261
  class CategoryHash < Hash
162
262
  include Hashie::Extensions::Coercion
163
263
  include Hashie::Extensions::MergeInitializer
164
264
 
165
- coerce_key :products, Array[ProductHash]
265
+ coerce_key :products, ->(value) do
266
+ return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map)
267
+
268
+ ProductHash.new(value)
269
+ end
166
270
  end
167
271
 
168
272
  class ProductHash < Hash
169
273
  include Hashie::Extensions::Coercion
170
274
  include Hashie::Extensions::MergeInitializer
171
275
 
172
- coerce_key :categories, Array[CategoriesHash]
276
+ coerce_key :categories, Array[CategoryHash]
173
277
  end
278
+
279
+ category = CategoryHash.new(products: [{ categories: [{}] }])
280
+ category[:products].first.class # => ProductHash
281
+ category[:products].first[:categories].first.class # => CategoryHash
174
282
  ```
175
283
 
176
- This will fail with a `NameError` for `CategoryHash::ProductHash` because `ProductHash` is not defined at the point that `coerce_key` is happening for `CategoryHash`.
284
+ #### Nil Values and Defaults
177
285
 
178
- To work around this, you can use a coercion proc. For example, you could do:
286
+ Top-level `nil` values bypass coercion: `coerce_key :description, String` does not turn `nil` into `''`. A coercion rule converts supplied non-nil values; it does not define a default. Use a Dash property default when missing or nil input should become an empty string:
179
287
 
180
288
  ```ruby
181
- class CategoryHash < Hash
182
- # ...
183
- coerce_key :products, ->(value) do
184
- return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map)
289
+ class DescriptionParams < Hashie::Dash
290
+ include Hashie::Extensions::Coercion
185
291
 
186
- ProductHash.new(value)
187
- end
292
+ property :description, default: ''
293
+ coerce_key :description, String
294
+ end
295
+
296
+ params = DescriptionParams.new(description: nil)
297
+ params.description # => ''
298
+ DescriptionParams.new.description # => ''
299
+ DescriptionParams.new(description: 123).description # => '123'
300
+
301
+ params.description = nil
302
+ params.description # => nil
303
+ params.update_attributes!(description: nil)
304
+ params.description # => ''
305
+ ```
306
+
307
+ Dash initialization and `update_attributes!` apply property defaults to nil values. Direct assignments through a property writer or `[]=` retain nil and do not reapply defaults. These rules also apply to subclasses. Dash already initializes attributes; it does not need `MergeInitializer`.
308
+
309
+ #### KernelCoercion
310
+
311
+ By default, `Hashie::Extensions::Coercion` coerces core types using lenient methods like `#to_i` and `#to_f`, which can silently produce undesirable results:
312
+
313
+ ```ruby
314
+ 'abcd'.to_f # => 0.0
315
+ ```
316
+
317
+ The `Hashie::Extensions::KernelCoercion` mixin extends `Coercion` to use the stricter `Kernel` methods (e.g. `Kernel#Integer`, `Kernel#Float`) instead, which raise an error for invalid input rather than silently coercing it:
318
+
319
+ ```ruby
320
+ class StrictHash < Hash
321
+ include Hashie::Extensions::Coercion
322
+ extend Hashie::Extensions::KernelCoercion
323
+
324
+ coerce_key :count, Integer
325
+ end
326
+
327
+ hash = StrictHash.new
328
+ hash[:count] = '5'
329
+ hash[:count] # => 5
330
+ begin
331
+ hash[:count] = 'abcd'
332
+ rescue Hashie::CoercionError
333
+ # Expected: Hashie::CoercionError, /cannot coerce property `:count`/
334
+ end
335
+ ```
336
+
337
+ This applies to `Integer`, `Float`, `Complex`, `Rational` and `String`. There is no strict `Kernel` method equivalent for `Symbol`, so `Symbol` coercion always uses the default, lenient `#to_sym` behavior.
338
+
339
+ #### Callable Coercion
340
+
341
+ Coercion also accepts bound methods and other objects responding to `call`. Existing collection and class/module dispatch takes precedence over `call` for non-Proc targets; other callable objects use `call` even if they also expose `coerce` or `new`. Callable coercers work with `coerce_key`, `coerce_value`, collection members, and Dash's `coerce:` property option.
342
+
343
+ Hashie does not load or depend on external type libraries. The callable determines conversion and validation; no additional output validation or empty-string normalization is applied. Hashie preserves its existing `nil` bypass for top-level assignments, even when the callable would reject `nil`; nested collection members are passed to their coercers. As with existing coercers, `TypeError`, `ArgumentError`, and `NoMethodError` during assignment become `Hashie::CoercionError`; other exceptions propagate unchanged.
344
+
345
+ #### Coercion with dry-types
346
+
347
+ Add `dry-types` to your application's Gemfile:
348
+
349
+ ```gemfile
350
+ gem 'dry-types'
351
+ ```
352
+
353
+ Pass a [dry-types](https://dry-rb.org/gems/dry-types/main/) object directly as the coercer:
354
+
355
+ ```ruby
356
+ require 'dry-types'
357
+ require 'hashie'
358
+
359
+ class Settings < Hashie::Dash
360
+ include Hashie::Extensions::Dash::Coercion
361
+
362
+ property :enabled, coerce: Dry::Types['params.bool']
363
+ end
364
+
365
+ Settings.new(enabled: 'false').enabled # => false
366
+ Settings.new(enabled: nil).enabled # => nil
367
+ begin
368
+ Settings.new(enabled: 'unknown')
369
+ rescue Dry::Types::CoercionError
370
+ # Expected: Dry::Types::CoercionError
371
+ end
372
+ ```
373
+
374
+ The `params.bool` type converts recognized boolean representations and rejects unknown values and empty strings. Use `Dry::Types['strict.bool']` for validation without conversion: it accepts `true` and `false`, but rejects strings such as `'false'`. Top-level `nil` assignments still bypass coercion.
375
+
376
+ The bypass applies only to top-level assignments, not collection members:
377
+
378
+ ```ruby
379
+ require 'dry-types'
380
+
381
+ class Flags < Hashie::Dash
382
+ include Hashie::Extensions::Dash::Coercion
383
+
384
+ property :values, coerce: Array[Dry::Types['strict.bool']]
385
+ end
386
+
387
+ Flags.new(values: nil).values # => nil
388
+ begin
389
+ Flags.new(values: [true, nil])
390
+ rescue Dry::Types::ConstraintError
391
+ # Expected: Dry::Types::ConstraintError
392
+ end
393
+ ```
394
+
395
+ #### Coercion with ActiveModel
396
+
397
+ Add `activemodel` to your application's Gemfile if it is not already provided by Rails:
398
+
399
+ ```gemfile
400
+ gem 'activemodel'
401
+ ```
402
+
403
+ Pass a bound [ActiveModel type](https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html) `cast` method as the coercer:
404
+
405
+ ```ruby
406
+ require 'active_model'
407
+ require 'hashie'
408
+
409
+ class Settings < Hashie::Dash
410
+ include Hashie::Extensions::Dash::Coercion
411
+
412
+ property :enabled, coerce: ActiveModel::Type::Boolean.new.method(:cast)
188
413
  end
414
+
415
+ Settings.new(enabled: 'off').enabled # => false
416
+ Settings.new(enabled: nil).enabled # => nil
417
+ Settings.new(enabled: '').enabled # => nil
418
+ Settings.new(enabled: 'unknown').enabled # => true
189
419
  ```
190
420
 
421
+ ActiveModel's boolean policy differs from dry-types: it treats an empty string as `nil`, recognized false values as false, and most other values as true. Choose the library whose conversion policy matches your application.
422
+
191
423
  ### KeyConversion
192
424
 
193
- The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or [`Hashie::Extensions::SymbolizeKeys`](#mash-extension-symbolizekeys).
425
+ The KeyConversion extension gives you the convenience methods of `symbolize_keys` and `stringify_keys` along with their bang counterparts. You can also include just stringify or just symbolize with `Hashie::Extensions::StringifyKeys` or `Hashie::Extensions::SymbolizeKeys`.
194
426
 
195
427
  Hashie also has a utility method for converting keys on a Hash without a mixin:
196
428
 
197
429
  ```ruby
198
- Hashie.symbolize_keys! hash # => Symbolizes keys of hash.
199
- Hashie.symbolize_keys hash # => Returns a copy of hash with keys symbolized.
200
- Hashie.stringify_keys! hash # => Stringifies keys of hash.
201
- Hashie.stringify_keys hash # => Returns a copy of hash with keys stringified.
430
+ hash = { 'name' => 'Ruby' }
431
+ Hashie.symbolize_keys(hash) # => { name: 'Ruby' }
432
+ hash # => { 'name' => 'Ruby' }
433
+ Hashie.symbolize_keys!(hash) # => { name: 'Ruby' }
434
+ Hashie.stringify_keys(hash) # => { 'name' => 'Ruby' }
435
+ hash # => { name: 'Ruby' }
436
+ Hashie.stringify_keys!(hash) # => { 'name' => 'Ruby' }
202
437
  ```
203
438
 
204
439
  ### MergeInitializer
205
440
 
206
441
  The MergeInitializer extension simply makes it possible to initialize a Hash subclass with another Hash, giving you a quick short-hand.
207
442
 
443
+ ### DeepMergeInitializer
444
+
445
+ The `DeepMergeInitializer` extension initializes a `Hash` subclass from another hash, like `MergeInitializer`, but also converts nested hashes to the same concrete class. This includes hashes inside arrays, even when arrays are nested.
446
+
447
+ <!-- hashie:example deep-merge-initializer -->
448
+ ```ruby
449
+ class MyHash < Hash
450
+ include Hashie::Extensions::DeepMergeInitializer
451
+ include Hashie::Extensions::IndifferentAccess
452
+ include Hashie::Extensions::MethodAccess
453
+ end
454
+
455
+ hash = MyHash.new(foo: { bar: 1 }, children: [{ name: 'Bob' }])
456
+ hash.foo.bar # => 1
457
+ hash['foo']['bar'] # => 1
458
+ hash.children.first.name # => 'Bob'
459
+ hash.foo.class # => MyHash
460
+ ```
461
+
462
+ Use this extension instead of `MergeInitializer` on plain `Hash` subclasses; Mash already provides recursive conversion, and Dash/Trash have their own property-aware initializers. The subclass must accept the same initializer arguments. Conversion happens only during initialization, not on later assignment or merge, and does not change `MethodAccess` itself.
463
+
464
+ An optional second argument supplies a default value, including `false`; alternatively, a block supplies a default proc. The same default is passed to every converted hash. As with `Hash`, a non-nil default value cannot be combined with a block.
465
+
466
+ <!-- hashie:example deep-merge-initializer -->
467
+ ```ruby
468
+ hash = MyHash.new({ foo: {} }, false)
469
+ hash[:missing] # => false
470
+ hash.foo[:missing] # => false
471
+ ```
472
+
473
+ Input hashes and arrays are rebuilt without modifying the source; keys and non-container values are retained. Use acyclic input data.
474
+
208
475
  ### MethodAccess
209
476
 
210
477
  The MethodAccess extension allows you to quickly build method-based reading, writing, and querying into your Hash descendant. It can also be included as individual modules, i.e. `Hashie::Extensions::MethodReader`, `Hashie::Extensions::MethodWriter` and `Hashie::Extensions::MethodQuery`.
@@ -220,6 +487,56 @@ h.abc # => 'def'
220
487
  h.abc? # => true
221
488
  ```
222
489
 
490
+ ### BlockAccess
491
+
492
+ `Hashie::Extensions::BlockAccess` builds nested hashes using blocks. It works with Hash subclasses, including Mash and Dash, without changing calls made without a block. For a plain Hash subclass, include `MethodAccess` first if you also want method-style readers and writers.
493
+
494
+ ```ruby
495
+ class Configuration < Hash
496
+ include Hashie::Extensions::MethodAccess
497
+ include Hashie::Extensions::BlockAccess
498
+ end
499
+
500
+ config = Configuration.new
501
+ config.author do |author|
502
+ author.name = 'Michael Bleigh'
503
+ end
504
+ config.author.name #=> 'Michael Bleigh'
505
+
506
+ config.test(:one) { |one| one.foo = 'bar' }
507
+ config.test(:two) { |two| two.hurp = 'derp' }
508
+ config.test.one.foo #=> 'bar'
509
+ config.test.two.hurp #=> 'derp'
510
+ ```
511
+
512
+ Include only `BlockAccess` in a Mash subclass; Mash already supplies method access. Bracket calls also accept a block: `config['author'] { |author| author.name = 'Bob' }`. Each call returns the child, not the block's result. Existing hashes are reused; missing children are constructed using the containing hash's class, which must accept no arguments. Existing non-hash values, including `nil` and `false`, raise `TypeError` rather than being overwritten.
513
+
514
+ Dash properties must be declared, and its constructor and property validations still apply. Real methods (such as `each`) and Mash's `?`, `!`, `=`, and `_` suffixes keep their existing behavior.
515
+
516
+ ### Persistable
517
+
518
+ `Hashie::Extensions::Persistable` adds opt-in YAML file persistence to classes whose constructors accept a hash. Plain Hash subclasses can include `MergeInitializer` first.
519
+
520
+ ```ruby
521
+ class Configuration < Hashie::Mash
522
+ include Hashie::Extensions::Persistable
523
+ end
524
+
525
+ config = Configuration.new(name: 'example')
526
+ config.save('config.yaml') # => File.expand_path('config.yaml')
527
+ loaded = Configuration.load('config.yaml')
528
+ loaded.name # => 'example'
529
+ loaded.name = 'updated'
530
+ loaded.save # writes back to config.yaml
531
+ Configuration.load('config.yaml').name # => 'updated'
532
+ ```
533
+
534
+ `save` returns and remembers the absolute path only after a successful write. It atomically replaces the destination using a temporary file in the same directory, preserves existing file permissions, and creates new files with private permissions. A failed write leaves the remembered path unchanged.
535
+
536
+ Files contain plain YAML data, not Ruby class tags or persistence metadata. Loading uses `Psych.safe_load`, permits symbols for declared Dash/Trash properties, rejects aliases and non-hash roots, and never evaluates ERB. String and symbol keys retain their types; Dash/Trash files must use keys accepted by their constructors. Nested Hashie classes are not encoded: constructors such as Mash may reconstruct their own nested types, while Dash retains nested plain hashes. Supported values are hashes, arrays, strings, symbols, numbers, booleans, and `nil`; cycles and unsupported objects are rejected.
537
+
538
+ Including this extension explicitly replaces an inherited class-level `load` on that subclass. Ordinary `Hashie::Mash.load` retains its existing caching and YAML/ERB behavior. No autosave or other serialization formats are provided.
539
+
223
540
  ### MethodAccessWithOverride
224
541
 
225
542
  The MethodAccessWithOverride extension is like the MethodAccess extension, except that it allows you to override Hash methods. It aliases any overridden method with two leading underscores. To include only this overriding functionality, you can include the single module `Hashie::Extensions::MethodOverridingWriter`.
@@ -269,8 +586,7 @@ This extension can be mixed in to your Hash subclass to allow you to use Strings
269
586
 
270
587
  In addition, IndifferentAccess will also inject itself into sub-hashes so they behave the same.
271
588
 
272
- Example:
273
-
589
+ <!-- hashie:example indifferent-access -->
274
590
  ```ruby
275
591
  class MyHash < Hash
276
592
  include Hashie::Extensions::MergeInitializer
@@ -290,12 +606,25 @@ myhash['fishes'][:food] = 'flakes'
290
606
  myhash['fishes']['food'] # => "flakes"
291
607
  ```
292
608
 
609
+ To get back a normal, not-indifferent Hash, you can use `#to_hash` on the indifferent hash. It exports the keys as strings, not symbols:
610
+
611
+ <!-- hashie:example indifferent-access -->
612
+ ```ruby
613
+ myhash = MyHash.new
614
+ myhash["foo"] = "bar"
615
+ myhash[:foo] #=> "bar"
616
+
617
+ normal_hash = myhash.to_hash
618
+ normal_hash["foo"] #=> "bar"
619
+ normal_hash[:foo] #=> nil
620
+ ```
621
+
293
622
  ### IgnoreUndeclared
294
623
 
295
624
  This extension can be mixed in to silently ignore undeclared properties on initialization instead of raising an error. This is useful when using a Trash to capture a subset of a larger hash.
296
625
 
297
626
  ```ruby
298
- class Person < Trash
627
+ class Person < Hashie::Trash
299
628
  include Hashie::Extensions::IgnoreUndeclared
300
629
  property :first_name
301
630
  property :last_name
@@ -311,13 +640,16 @@ p = Person.new(user_data) # 'email' is silently ignored
311
640
 
312
641
  p.first_name # => 'Freddy'
313
642
  p.last_name # => 'Nostrils'
314
- p.email # => NoMethodError
643
+ begin
644
+ p.email
645
+ rescue NoMethodError
646
+ # Expected: NoMethodError
647
+ end
315
648
  ```
316
649
 
317
650
  ### DeepMerge
318
651
 
319
- This extension allows you to easily include a recursive merging
320
- system into any Hash descendant:
652
+ Include `DeepMerge` to merge nested hashes recursively:
321
653
 
322
654
  ```ruby
323
655
  class MyHash < Hash
@@ -341,11 +673,9 @@ end
341
673
  h1 = MyHash[{ a: 100, b: 200, c: { c1: 100 } }]
342
674
  h2 = MyHash[{ b: 250, c: { c1: 200 } }]
343
675
 
344
- h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
345
- # => { a: 100, b: 450, c: { c1: 300 } }
676
+ h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } # => { a: 100, b: 450, c: { c1: 300 } }
346
677
  ```
347
678
 
348
-
349
679
  ### DeepFetch
350
680
 
351
681
  This extension can be mixed in to provide for safe and concise retrieval of deeply nested hash values. In the event that the requested key does not exist a block can be provided and its value will be returned.
@@ -364,7 +694,11 @@ user = {
364
694
  user.extend Hashie::Extensions::DeepFetch
365
695
 
366
696
  user.deep_fetch :name, :first # => 'Bob'
367
- user.deep_fetch :name, :middle # => 'KeyError: Could not fetch middle'
697
+ begin
698
+ user.deep_fetch :name, :middle
699
+ rescue Hashie::Extensions::DeepFetch::UndefinedPathError
700
+ # Expected: Hashie::Extensions::DeepFetch::UndefinedPathError, /could not fetch path .* at `middle`/
701
+ end
368
702
 
369
703
  # using a default block
370
704
  user.deep_fetch(:name, :middle) { |key| 'default' } # => 'default'
@@ -399,11 +733,39 @@ user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'O
399
733
  user.deep_select(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
400
734
  ```
401
735
 
402
- ### DeepLocate
736
+ ### DeepGrep
403
737
 
404
- This extension can be mixed in to provide a depth first search based search for enumerables matching a given comparator callable.
738
+ Search nested hashes and arrays for string or symbol keys and values matching a regular expression. Include `Hashie::Extensions::DeepGrep` to call `deep_grep(pattern)`.
405
739
 
406
- It returns all enumerables which contain at least one element, for which the given comparator returns ```true```.
740
+ ```ruby
741
+ class Library < Hash
742
+ include Hashie::Extensions::DeepGrep
743
+ end
744
+
745
+ books = Library[
746
+ 'Uncategorized' => [
747
+ { title: 'Ruby for beginners', pages: 120 },
748
+ { title: 'CSS for intermediates', pages: 80 }
749
+ ],
750
+ 'Collection of Ruby books' => [{ title: 'That other language by Matz', pages: 578 }]
751
+ ]
752
+
753
+ matches = [
754
+ { title: 'Ruby for beginners', pages: 120 },
755
+ { 'Collection of Ruby books' => [{ title: 'That other language by Matz', pages: 578 }] }
756
+ ]
757
+ books.deep_grep(/ruby/i) # => matches
758
+
759
+ books.deep_grep(/missing/) # => []
760
+ ```
761
+
762
+ For plain hashes or arrays, call `Hashie::Extensions::DeepGrep.deep_grep(pattern, object)` directly.
763
+
764
+ A matching hash key returns a new one-entry hash containing that key and its original subtree; that subtree is not searched further. A matching scalar hash value returns its containing hash once, preserving sibling context. A matching scalar array element returns just that element, not the whole array. Non-text scalars and container keys are not searched. Results follow depth-first insertion order; shared containers can appear at multiple input locations, and cycles are skipped. Input data is not modified, but returned containers share references with the input.
765
+
766
+ ### DeepLocate
767
+
768
+ `DeepLocate` searches nested Enumerables depth-first and returns each container with at least one element matching the comparator. The comparator receives the key, value, and containing object.
407
769
 
408
770
  Because the container objects are returned, the result elements can be modified in place. This way, one can perform modifications on deeply nested hashes without the need to know the exact paths.
409
771
 
@@ -431,40 +793,34 @@ books = [
431
793
 
432
794
  books.extend(Hashie::Extensions::DeepLocate)
433
795
 
434
- # for ruby 1.9 leave *no* space between the lambda rocket and the braces
435
- # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
436
-
437
- books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
438
- # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
796
+ books.deep_locate ->(key, value, _object) { key == :title && value.include?("Ruby") } # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
439
797
 
440
- books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
441
- # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
798
+ books.deep_locate ->(key, value, _object) { key == :pages && value <= 120 } # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
442
799
  ```
443
800
 
444
- ## StrictKeyAccess
801
+ Traversal enumerates nested objects, including files and other stateful Enumerables, and can consume their contents. Use this extension only when enumerating every nested Enumerable is safe. A matching comparator does not stop traversal into that container.
445
802
 
446
- This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key.
803
+ ### StrictKeyAccess
447
804
 
448
- ### Example:
805
+ This extension can be mixed in to allow a Hash to raise an error when attempting to extract a value using a non-existent key.
449
806
 
450
807
  ```ruby
451
808
  class StrictKeyAccessHash < Hash
452
809
  include Hashie::Extensions::StrictKeyAccess
453
810
  end
454
811
 
455
- >> hash = StrictKeyAccessHash[foo: "bar"]
456
- => {:foo=>"bar"}
457
- >> hash[:foo]
458
- => "bar"
459
- >> hash[:cow]
460
- KeyError: key not found: :cow
812
+ hash = StrictKeyAccessHash[foo: "bar"] # => {:foo=>"bar"}
813
+ hash[:foo] # => "bar"
814
+ begin
815
+ hash[:cow]
816
+ rescue KeyError
817
+ # Expected: KeyError, "key not found: :cow"
818
+ end
461
819
  ```
462
820
 
463
821
  ## Mash
464
822
 
465
- Mash is an extended Hash that gives simple pseudo-object functionality that can be built from hashes and easily extended. It is intended to give the user easier access to the objects within the Mash through a property-like syntax, while still retaining all Hash functionality.
466
-
467
- ### Example:
823
+ Mash adds method-style access to Hash keys and recursively wraps nested hashes:
468
824
 
469
825
  ```ruby
470
826
  mash = Hashie::Mash.new
@@ -473,39 +829,34 @@ mash.name # => nil
473
829
  mash.name = "My Mash"
474
830
  mash.name # => "My Mash"
475
831
  mash.name? # => true
476
- mash.inspect # => <Hashie::Mash name="My Mash">
832
+ mash.inspect # => '#<Hashie::Mash name="My Mash">'
477
833
 
478
834
  mash = Hashie::Mash.new
479
835
  # use bang methods for multi-level assignment
480
836
  mash.author!.name = "Michael Bleigh"
481
- mash.author # => <Hashie::Mash name="Michael Bleigh">
837
+ mash.author.to_h # => { 'name' => 'Michael Bleigh' }
482
838
 
483
839
  mash = Hashie::Mash.new
484
840
  # use under-bang methods for multi-level testing
485
841
  mash.author_.name? # => false
486
- mash.inspect # => <Hashie::Mash>
842
+ mash.inspect # => '#<Hashie::Mash>'
487
843
  ```
488
844
 
489
845
  **Note:** The `?` method will return false if a key has been set to false or nil. In order to check if a key has been set at all, use the `mash.key?('some_key')` method instead.
490
846
 
491
- ### How does Mash handle conflicts with pre-existing methods?
492
-
493
- Please note that a Mash will not override methods through the use of the property-like syntax. This can lead to confusion if you expect to be able to access a Mash value through the property-like syntax for a key that conflicts with a method name. However, it protects users of your library from the unexpected behavior of those methods being overridden behind the scenes.
847
+ ### Method Conflicts
494
848
 
495
- #### Example:
849
+ Mash does not override existing methods. Use brackets to read a key that conflicts with a method name:
496
850
 
497
851
  ```ruby
498
852
  mash = Hashie::Mash.new
499
853
  mash.name = "My Mash"
500
854
  mash.zip = "Method Override?"
501
855
  mash.zip # => [[["name", "My Mash"]], [["zip", "Method Override?"]]]
856
+ mash[:zip] # => 'Method Override?'
502
857
  ```
503
858
 
504
- Since Mash gives you the ability to set arbitrary keys that then act as methods, Hashie logs when there is a conflict between a key and a pre-existing method. You can set the logger that this logs message to via the global Hashie logger:
505
-
506
- ```ruby
507
- Hashie.logger = Rails.logger
508
- ```
859
+ Hashie logs a warning when a key conflicts with an existing method. See [Logging](#logging) to configure the logger.
509
860
 
510
861
  You can also disable the logging in subclasses of Mash:
511
862
 
@@ -513,14 +864,19 @@ You can also disable the logging in subclasses of Mash:
513
864
  class Response < Hashie::Mash
514
865
  disable_warnings
515
866
  end
867
+
868
+ Response.new(zip: '90210').zip # => [[['zip', '90210']]]
869
+ Response.disable_warnings? # => true
516
870
  ```
517
871
 
518
- The default is to disable logging for all methods that conflict. If you would like to only disable the logging for specific methods, you can include an array of method keys:
872
+ To suppress warnings only for specific methods, pass their names to `disable_warnings`:
519
873
 
520
874
  ```ruby
521
875
  class Response < Hashie::Mash
522
876
  disable_warnings :zip, :zap
523
877
  end
878
+
879
+ Response.disabled_warnings # => [:zip, :zap]
524
880
  ```
525
881
 
526
882
  This behavior is cumulative. The examples above and below behave identically.
@@ -530,21 +886,29 @@ class Response < Hashie::Mash
530
886
  disable_warnings :zip
531
887
  disable_warnings :zap
532
888
  end
889
+
890
+ Response.disabled_warnings # => [:zip, :zap]
533
891
  ```
534
892
 
535
- Disable warnings will honor the last `disable_warnings` call. Calling without parameters will override the ignored methods list, and calling with parameters will create a new ignored methods list. This includes child classes that inherit from a class that disables warnings.
893
+ Calling `disable_warnings` without arguments suppresses all warnings. A subsequent call with method names replaces that setting with a list of specific exceptions, including in subclasses:
536
894
 
537
895
  ```ruby
896
+ require 'stringio'
897
+
538
898
  class Message < Hashie::Mash
539
899
  disable_warnings :zip, :zap
540
900
  disable_warnings
541
901
  end
542
902
 
543
- # No errors will be logged
903
+ log = StringIO.new
904
+ Hashie.logger = Logger.new(log)
544
905
  Message.new(merge: 'true', compact: true)
906
+ log.string # => ''
545
907
  ```
546
908
 
547
909
  ```ruby
910
+ require 'stringio'
911
+
548
912
  class Message < Hashie::Mash
549
913
  disable_warnings
550
914
  end
@@ -553,22 +917,31 @@ class Response < Message
553
917
  disable_warnings :zip, :zap
554
918
  end
555
919
 
556
- # 2 errors will be logged
920
+ log = StringIO.new
921
+ Hashie.logger = Logger.new(log)
557
922
  Response.new(merge: 'true', compact: true, zip: '90210', zap: 'electric')
923
+ log.string.lines.size # => 2
924
+ log.string.include?('#merge ') # => true
925
+ log.string.include?('#compact ') # => true
558
926
  ```
559
927
 
560
928
  If you would like to create an anonymous subclass of a Hashie::Mash with key conflict warnings disabled:
561
929
 
562
930
  ```ruby
563
- Hashie::Mash.quiet.new(zip: '90210', compact: true) # no errors logged
564
- Hashie::Mash.quiet(:zip).new(zip: '90210', compact: true) # error logged for compact
931
+ require 'stringio'
932
+
933
+ log = StringIO.new
934
+ Hashie.logger = Logger.new(log)
935
+ Hashie::Mash.quiet.new(zip: '90210', compact: true)
936
+ log.string # => ''
937
+ Hashie::Mash.quiet(:zip).new(zip: '90210', compact: true)
938
+ log.string.lines.size # => 1
939
+ log.string.include?('#compact ') # => true
565
940
  ```
566
941
 
567
- ### How does the wrapping of Mash sub-Hashes work?
568
-
569
- Mash duplicates any sub-Hashes that you add to it and wraps them in a Mash. This allows for infinite chaining of nested Hashes within a Mash without modifying the object(s) that are passed into the Mash. When you subclass Mash, the subclass wraps any sub-Hashes in its own class. This preserves any extensions that you mixed into the Mash subclass and allows them to work within the sub-Hashes, in addition to the main containing Mash.
942
+ ### Nested Hashes
570
943
 
571
- #### Example:
944
+ Mash wraps nested plain hashes in new Mashes without modifying the input. A Mash subclass uses its own class for those nested hashes, preserving the extensions you included:
572
945
 
573
946
  ```ruby
574
947
  mash = Hashie::Mash.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" })
@@ -579,69 +952,130 @@ my_gem = MyGem.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0"
579
952
  my_gem.dependencies.class #=> MyGem
580
953
  ```
581
954
 
582
- ### What else can Mash do?
955
+ ### Key Types
956
+
957
+ Mash uses string keys for indifferent string/symbol access. Other key types, such as integers, are preserved and accessed with brackets:
958
+
959
+ ```ruby
960
+ Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num') # => {"1"=>"one sym", 1=>"one num"}
961
+ ```
962
+
963
+ The symbol `:'1'` and string `'1'` share a key, so the later value wins. The integer `1` remains a separate key.
583
964
 
584
- Mash allows you also to transform any files into a Mash objects.
965
+ ### Loading Files
585
966
 
586
- #### Example:
967
+ Use `Mash.load` to read a YAML configuration file:
587
968
 
588
969
  ```yml
589
- #/etc/config/settings/twitter.yml
970
+ # settings/twitter.yml
590
971
  development:
591
972
  api_key: 'api_key'
592
973
  production:
593
- api_key: <%= ENV['API_KEY'] %> #let's say that ENV['API_KEY'] is set to 'abcd'
974
+ api_key: <%= ENV.fetch('HASHIE_README_API_KEY') %> # set to 'abcd' below
594
975
  ```
595
976
 
977
+ <!-- hashie:example mash-load -->
596
978
  ```ruby
597
- mash = Mash.load('settings/twitter.yml')
598
- mash.development.api_key # => 'localhost'
599
- mash.development.api_key = "foo" # => <# RuntimeError can't modify frozen ...>
979
+ require 'fileutils'
980
+ require 'pathname'
981
+
982
+ FileUtils.mkdir_p('settings')
983
+ File.write('settings/twitter.yml', <<~YAML)
984
+ development:
985
+ api_key: 'api_key'
986
+ production:
987
+ api_key: <%= ENV.fetch('HASHIE_README_API_KEY') %>
988
+ YAML
989
+ ENV['HASHIE_README_API_KEY'] = 'abcd'
990
+
991
+ mash = Hashie::Mash.load('settings/twitter.yml')
992
+ mash.development.api_key # => 'api_key'
993
+ begin
994
+ mash.production = {}
995
+ rescue FrozenError
996
+ # Expected: FrozenError, /can't modify frozen/
997
+ end
600
998
  mash.development.api_key? # => true
999
+ mash.development.api_key = "foo"
1000
+ mash.development.api_key # => 'foo'
601
1001
  ```
602
1002
 
603
1003
  You can also load with a `Pathname` object:
604
1004
 
1005
+ <!-- hashie:example mash-load -->
1006
+ ```ruby
1007
+ mash = Hashie::Mash.load(Pathname 'settings/twitter.yml')
1008
+ mash.development.api_key # => 'api_key'
1009
+ ```
1010
+
1011
+ The loaded root is frozen, but nested Mashes remain mutable. Repeated loads with the same path object return the cached Mash.
1012
+
1013
+ By default, `Mash.load` evaluates ERB before parsing YAML. Only use it with trusted files; `YAML.safe_load` does not make ERB evaluation safe. To parse untrusted or plain YAML without evaluating ERB, explicitly select `YamlParser`:
1014
+
605
1015
  ```ruby
606
- mash = Mash.load(Pathname 'settings/twitter.yml')
607
- mash.development.api_key # => 'localhost'
1016
+ File.write('data.yml', "api_key: <%= not_evaluated %>\n")
1017
+ mash = Hashie::Mash.load('data.yml', parser: Hashie::Extensions::Parsers::YamlParser)
1018
+ mash.api_key # => '<%= not_evaluated %>'
608
1019
  ```
609
1020
 
610
1021
  You can access a Mash from another class:
611
1022
 
1023
+ <!-- hashie:example mash-load -->
612
1024
  ```ruby
613
- mash = Mash.load('settings/twitter.yml')[ENV['RACK_ENV']]
1025
+ class Twitter
1026
+ end
1027
+
1028
+ mash = Hashie::Mash.load('settings/twitter.yml')['production']
614
1029
  Twitter.extend mash.to_module # NOTE: if you want another name than settings, call: to_module('my_settings')
615
1030
  Twitter.settings.api_key # => 'abcd'
1031
+ ENV.delete('HASHIE_README_API_KEY')
616
1032
  ```
617
1033
 
618
- You can use another parser (by default: [YamlErbParser](lib/hashie/extensions/parsers/yaml_erb_parser.rb)):
1034
+ You can use another parser (by default: [YamlErbParser](lib/hashie/extensions/parsers/yaml_erb_parser.rb), which evaluates ERB):
619
1035
 
620
- ```
621
- #/etc/data/user.csv
622
- id | name | lastname
623
- ---|------------- | -------------
624
- 1 |John | Doe
625
- 2 |Laurent | Garnier
1036
+ ```text
1037
+ # data/user.csv
1038
+ id,name,lastname
1039
+ 1,John,Doe
1040
+ 2,Laurent,Garnier
626
1041
  ```
627
1042
 
628
1043
  ```ruby
629
- mash = Mash.load('data/user.csv', parser: MyCustomCsvParser)
630
- # => { 1 => { name: 'John', lastname: 'Doe'}, 2 => { name: 'Laurent', lastname: 'Garnier' } }
631
- mash[1] #=> { name: 'John', lastname: 'Doe' }
1044
+ require 'csv'
1045
+ require 'fileutils'
1046
+
1047
+ class MyCustomCsvParser
1048
+ def self.perform(path, _options = {})
1049
+ CSV.read(path, headers: true).to_h do |row|
1050
+ [row['id'].to_i, { 'name' => row['name'], 'lastname' => row['lastname'] }]
1051
+ end
1052
+ end
1053
+ end
1054
+
1055
+ FileUtils.mkdir_p('data')
1056
+ File.write('data/user.csv', "id,name,lastname\n1,John,Doe\n2,Laurent,Garnier\n")
1057
+ mash = Hashie::Mash.load('data/user.csv', parser: MyCustomCsvParser)
1058
+ mash.to_h # => { 1 => { 'name' => 'John', 'lastname' => 'Doe' }, 2 => { 'name' => 'Laurent', 'lastname' => 'Garnier' } }
1059
+ mash[1].to_h #=> { 'name' => 'John', 'lastname' => 'Doe' }
632
1060
  ```
633
1061
 
634
- The `Mash#load` method calls `YAML.safe_load(path, [], [], true)`.
1062
+ Both built-in YAML parsers use `YAML.safe_load`.
635
1063
 
636
1064
  Specify `permitted_symbols`, `permitted_classes` and `aliases` options as needed.
637
1065
 
638
1066
  ```ruby
639
- Mash.load('data/user.csv', permitted_classes: [Symbol], permitted_symbols: [], aliases: false)
1067
+ File.write('symbols.yml', ":name: Ruby\n")
1068
+ mash = Hashie::Mash.load('symbols.yml', permitted_classes: [Symbol], permitted_symbols: [], aliases: false)
1069
+ mash.name # => 'Ruby'
640
1070
  ```
641
1071
 
642
- ### Mash Extension: KeepOriginalKeys
1072
+ ### Mash and Rails 4 Strong Parameters
1073
+
1074
+ To enable compatibility with Rails 4 use the [hashie-forbidden_attributes](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes) gem.
643
1075
 
644
- This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.
1076
+ ### KeepOriginalKeys
1077
+
1078
+ This extension can be mixed into a Mash to keep the form of any keys passed directly into the Mash. By default, Mash converts symbol keys to strings to give indifferent access. This extension still allows indifferent access, but keeps the form of the keys to eliminate confusion when you're not expecting the keys to change.
645
1079
 
646
1080
  ```ruby
647
1081
  class KeepingMash < ::Hashie::Mash
@@ -658,7 +1092,7 @@ mash['string_key'] #=> 'string'
658
1092
  mash[:string_key] #=> 'string'
659
1093
  ```
660
1094
 
661
- ### Mash Extension: PermissiveRespondTo
1095
+ ### PermissiveRespondTo
662
1096
 
663
1097
  By default, Mash only states that it responds to built-in methods, affixed methods (e.g. setters, underbangs, etc.), and keys that it currently contains. That means it won't state that it responds to a getter for an unset key, as in the following example:
664
1098
 
@@ -680,28 +1114,70 @@ mash = PermissiveMash.new(a: 1)
680
1114
  mash.respond_to? :b #=> true
681
1115
  ```
682
1116
 
683
- This comes at the cost of approximately 20% performance for initialization and setters and 19KB of permanent memory growth for each such class that you create.
1117
+ Use this only when the delegator needs to forward reads for keys that have not been set.
684
1118
 
685
- ### Mash Extension: SafeAssignment
1119
+ ### SafeAssignment
686
1120
 
687
1121
  This extension can be mixed into a Mash to guard the attempted overwriting of methods by property setters. When mixed in, the Mash will raise an `ArgumentError` if you attempt to write a property with the same name as an existing method.
688
1122
 
689
- #### Example:
690
-
691
1123
  ```ruby
692
1124
  class SafeMash < ::Hashie::Mash
693
1125
  include Hashie::Extensions::Mash::SafeAssignment
694
1126
  end
695
1127
 
696
1128
  safe_mash = SafeMash.new
697
- safe_mash.zip = 'Test' # => ArgumentError
698
- safe_mash[:zip] = 'test' # => still ArgumentError
1129
+ begin
1130
+ safe_mash.zip = 'Test'
1131
+ rescue ArgumentError
1132
+ # Expected: ArgumentError, /clashes/
1133
+ end
1134
+ begin
1135
+ safe_mash[:zip] = 'test'
1136
+ rescue ArgumentError
1137
+ # Expected: ArgumentError, /clashes/
1138
+ end
1139
+ ```
1140
+
1141
+ ### TransformKeys
1142
+
1143
+ Include `Hashie::Extensions::Mash::TransformKeys` and configure `transform_keys_with` with a callable or block to normalize string and symbol keys. The transformer receives a string and must return a string; other key types are unchanged.
1144
+
1145
+ ```ruby
1146
+ class LowercaseMash < Hashie::Mash
1147
+ include Hashie::Extensions::Mash::TransformKeys
1148
+ transform_keys_with(&:downcase)
1149
+ end
1150
+
1151
+ mash = LowercaseMash.new('User' => { 'NAME' => 'Ada' })
1152
+ mash.user.name # => 'Ada'
1153
+ mash.USER.NAME # => 'Ada'
1154
+ ```
1155
+
1156
+ Normalization applies to reads and writes, including nested Mashes and hashes inside arrays. Subclasses inherit the callable but can replace it without changing their parent or siblings. Without a callable, ordinary Mash key conversion is retained. Configure the transformer before creating instances, keep it free of side effects, and make it idempotent: transforming an already-normalized key must return the same key. When input keys normalize to the same key, the last assignment wins. Transformer exceptions propagate; non-string results raise `TypeError`.
1157
+
1158
+ ### UnderscoreKeys
1159
+
1160
+ Include `Hashie::Extensions::Mash::UnderscoreKeys` to normalize camelCase, PascalCase, and hyphenated keys to lowercase underscore names. It uses `TransformKeys`, including its nested conversion and subclass configuration behavior.
1161
+
1162
+ ```ruby
1163
+ class UnderscoreMash < Hashie::Mash
1164
+ include Hashie::Extensions::Mash::UnderscoreKeys
1165
+ end
1166
+
1167
+ mash = UnderscoreMash.new('updatedAt' => 'today', 'created-at' => 'tomorrow')
1168
+ mash.updatedAt # => 'today'
1169
+ mash.updated_at # => 'today'
1170
+ mash.created_at # => 'tomorrow'
1171
+ mash.to_h # => { 'updated_at' => 'today', 'created_at' => 'tomorrow' }
699
1172
  ```
700
1173
 
701
- ### Mash Extension: SymbolizeKeys
1174
+ Acronym boundaries are split: `HTTPServer` becomes `http_server`, and `GitHub` becomes `git_hub`. There is no acronym registry or ActiveSupport dependency. Spaces and punctuation other than hyphens are retained, so this does not guarantee method-safe names. Non-text keys are unchanged. If keys normalize to the same name, the last assignment wins. Use `transform_keys_with` to replace this policy with custom normalization.
702
1175
 
703
- This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all keys to symbols. It can be useful to use with keywords argument, which required symbol keys.
1176
+ ### SymbolizeKeys
704
1177
 
1178
+ Include `Hashie::Extensions::Mash::SymbolizeKeys` to store string keys as symbols instead of converting symbols to strings. This is useful when passing a Mash as keyword arguments:
1179
+
1180
+ <!-- hashie:example symbolized-mash -->
705
1181
  ```ruby
706
1182
  class SymbolizedMash < ::Hashie::Mash
707
1183
  include Hashie::Extensions::Mash::SymbolizeKeys
@@ -713,27 +1189,27 @@ symbol_mash.test #=> 'value'
713
1189
  symbol_mash.to_h #=> {test: 'value'}
714
1190
 
715
1191
  def example(test:)
716
- puts test
1192
+ test
717
1193
  end
718
1194
 
719
- example(symbol_mash) #=> value
1195
+ example(**symbol_mash) #=> 'value'
720
1196
  ```
721
1197
 
722
- There is a major benefit and coupled with a major trade-off to this decision (at least on older Rubies). As a benefit, by using symbols as keys, you will be able to use the implicit conversion of a Mash via the `#to_hash` method to destructure (or splat) the contents of a Mash out to a block. This can be handy for doing iterations through the Mash's keys and values, as follows:
1198
+ Iteration also returns symbol keys:
723
1199
 
1200
+ <!-- hashie:example symbolized-mash -->
724
1201
  ```ruby
725
1202
  symbol_mash = SymbolizedMash.new(id: 123, name: 'Rey')
1203
+ entries = []
726
1204
  symbol_mash.each do |key, value|
727
- # key is :id, then :name
728
- # value is 123, then 'Rey'
1205
+ entries << [key, value]
729
1206
  end
1207
+ entries # => [[:id, 123], [:name, 'Rey']]
730
1208
  ```
731
1209
 
732
- However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input.
733
-
734
- ### Mash Extension: DefineAccessors
1210
+ ### DefineAccessors
735
1211
 
736
- This extension can be mixed into a Mash so it makes it behave like `OpenStruct`. It reduces the overhead of `method_missing?` magic by lazily defining field accessors when they're requested.
1212
+ Include `Hashie::Extensions::Mash::DefineAccessors` to lazily define readers and writers when first used, reducing repeated dispatch through `method_missing`.
737
1213
 
738
1214
  ```ruby
739
1215
  class MyHash < ::Hashie::Mash
@@ -754,17 +1230,76 @@ You can also extend the existing mash without defining a class:
754
1230
 
755
1231
  ```ruby
756
1232
  mash = ::Hashie::Mash.new.with_accessors!
1233
+ mash.foo = 123
1234
+ mash.singleton_class.method_defined?(:foo=) # => true
1235
+ mash.foo # => 123
1236
+ mash.singleton_class.method_defined?(:foo) # => true
1237
+ ```
1238
+
1239
+ ### StrictKeyAccess
1240
+
1241
+ This extension can be mixed into a Mash to raise a `KeyError` for any key that is not set, instead of returning `nil`. It's aware of Mash's special suffixed methods (`?`, `!`, `_` and `=`), which all continue to work as normal. Since nested Hashes are converted into instances of the same Mash class, this behavior automatically applies to nested Mashes as well.
1242
+
1243
+ ```ruby
1244
+ class StrictMash < Hashie::Mash
1245
+ include Hashie::Extensions::Mash::StrictKeyAccess
1246
+ end
1247
+
1248
+ mash = StrictMash.new(name: 'Bob', address: { city: 'Springfield' })
1249
+ mash.name #=> 'Bob'
1250
+ begin
1251
+ mash.age
1252
+ rescue KeyError
1253
+ # Expected: KeyError, 'key not found: `age`'
1254
+ end
1255
+
1256
+ mash.name? #=> true
1257
+ mash.age? #=> false
1258
+
1259
+ mash.address.city #=> 'Springfield'
1260
+ begin
1261
+ mash.address.postcode
1262
+ rescue KeyError
1263
+ # Expected: KeyError, 'key not found: `postcode`'
1264
+ end
1265
+ ```
1266
+
1267
+
1268
+ ### RequiredKeys
1269
+
1270
+ This extension can be mixed into a Mash to require specific keys to be set (non-nil) when constructing an instance, while still allowing arbitrary, undeclared keys to be assigned - unlike `Hashie::Dash`, which only allows setting pre-declared properties.
1271
+
1272
+ ```ruby
1273
+ class PersonMash < ::Hashie::Mash
1274
+ include Hashie::Extensions::Mash::RequiredKeys
1275
+
1276
+ required_keys :name, :email
1277
+ end
1278
+
1279
+ begin
1280
+ PersonMash.new(email: 'bob@example.com')
1281
+ rescue ArgumentError
1282
+ # Expected: ArgumentError, 'the following keys are required: `name`'
1283
+ end
1284
+
1285
+ person = PersonMash.new(name: 'Bob', email: 'bob@example.com')
1286
+ person.age = 42 # arbitrary, undeclared keys are still allowed
1287
+ person.age # => 42
757
1288
  ```
758
1289
 
759
1290
  ## Dash
760
1291
 
761
- Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property.
1292
+ Dash accepts only declared properties. Use `default:` for initial values, `required: true` to reject missing or nil values, and `message:` to customize a required-property error. A proc assigned as a property's value is evaluated when read; proc defaults are evaluated during initialization.
762
1293
 
763
1294
  You can also conditionally require certain properties by passing a Proc or Symbol. If a Proc is provided, it will be run in the context of the Dash instance. If a Symbol is provided, the value returned for the property or method of the same name will be evaluated. The property will be required if the result of the conditional is truthy.
764
1295
 
765
- ### Example:
766
-
767
1296
  ```ruby
1297
+ class Genome
1298
+ def self.sequence
1299
+ 'GATTACA'
1300
+ end
1301
+ end
1302
+
768
1303
  class Person < Hashie::Dash
769
1304
  property :name, required: true
770
1305
  property :age, required: true, message: 'must be set.'
@@ -772,36 +1307,57 @@ class Person < Hashie::Dash
772
1307
  property :phone, required: -> { email.nil? }, message: 'is required if email is not set.'
773
1308
  property :pants, required: :weekday?, message: 'are only required on weekdays.'
774
1309
  property :occupation, default: 'Rubyist'
1310
+ property :genome
775
1311
 
776
1312
  def weekday?
777
1313
  [ Time.now.saturday?, Time.now.sunday? ].none?
778
1314
  end
779
1315
  end
780
1316
 
781
- p = Person.new # => ArgumentError: The property 'name' is required for this Dash.
782
- p = Person.new(name: 'Bob') # => ArgumentError: The property 'age' must be set.
1317
+ begin
1318
+ p = Person.new
1319
+ rescue ArgumentError
1320
+ # Expected: ArgumentError, /the property `name` is required for/
1321
+ end
1322
+ begin
1323
+ p = Person.new(name: 'Bob')
1324
+ rescue ArgumentError
1325
+ # Expected: ArgumentError, "the property `age` must be set."
1326
+ end
783
1327
 
784
- p = Person.new(name: "Bob", age: 18)
1328
+ p = Person.new(name: "Bob", age: 18, email: 'bob@example.com', pants: true)
785
1329
  p.name # => 'Bob'
786
- p.name = nil # => ArgumentError: The property 'name' is required for this Dash.
1330
+ begin
1331
+ p.name = nil
1332
+ rescue ArgumentError
1333
+ # Expected: ArgumentError, /the property `name` is required for/
1334
+ end
787
1335
  p.age # => 18
788
- p.age = nil # => ArgumentError: The property 'age' must be set.
1336
+ begin
1337
+ p.age = nil
1338
+ rescue ArgumentError
1339
+ # Expected: ArgumentError, "the property `age` must be set."
1340
+ end
789
1341
  p.email = 'abc@def.com'
790
1342
  p.occupation # => 'Rubyist'
791
1343
  p.email # => 'abc@def.com'
792
- p[:awesome] # => NoMethodError
1344
+ begin
1345
+ p[:awesome]
1346
+ rescue NoMethodError
1347
+ # Expected: NoMethodError
1348
+ end
793
1349
  p[:occupation] # => 'Rubyist'
794
1350
  p.update_attributes!(name: 'Trudy', occupation: 'Evil')
795
1351
  p.occupation # => 'Evil'
796
1352
  p.name # => 'Trudy'
797
1353
  p.update_attributes!(occupation: nil)
798
1354
  p.occupation # => 'Rubyist'
1355
+ p.genome = -> { Genome.sequence } # Some expensive operation
1356
+ p.genome # => 'GATTACA'
799
1357
  ```
800
1358
 
801
1359
  Properties defined as symbols are not the same thing as properties defined as strings.
802
1360
 
803
- ### Example:
804
-
805
1361
  ```ruby
806
1362
  class Tricky < Hashie::Dash
807
1363
  property :trick
@@ -809,12 +1365,12 @@ class Tricky < Hashie::Dash
809
1365
  end
810
1366
 
811
1367
  p = Tricky.new(trick: 'one', 'trick' => 'two')
812
- p.trick # => 'one', always symbol version
1368
+ p.trick # => 'two'
813
1369
  p[:trick] # => 'one'
814
1370
  p['trick'] # => 'two'
815
1371
  ```
816
1372
 
817
- Note that accessing a property as a method always uses the symbol version.
1373
+ Bracket access distinguishes symbol and string properties. When both declarations generate the same method name, the last declaration supplies the method accessor.
818
1374
 
819
1375
  ```ruby
820
1376
  class Tricky < Hashie::Dash
@@ -822,13 +1378,122 @@ class Tricky < Hashie::Dash
822
1378
  end
823
1379
 
824
1380
  p = Tricky.new('trick' => 'two')
825
- p.trick # => NoMethodError
1381
+ p.trick # => 'two'
826
1382
  ```
827
1383
 
828
- ### Potential gotchas
1384
+ If you would like to update a Dash and use any default values set in the case of a `nil` value, use `#update_attributes!`.
829
1385
 
830
- Because Dashes are subclasses of the built-in Ruby Hash class, the double-splat operator takes the Dash as-is without any conversion. This can lead to strange behavior when you use the double-splat operator on a Dash as the first part of a keyword list or built Hash. For example:
1386
+ ```ruby
1387
+ class WithDefaults < Hashie::Dash
1388
+ property :description, default: 'none'
1389
+ end
1390
+
1391
+ dash = WithDefaults.new
1392
+ dash.description #=> 'none'
1393
+
1394
+ dash.description = 'You committed one of the classic blunders!'
1395
+ dash.description #=> 'You committed one of the classic blunders!'
831
1396
 
1397
+ dash.description = nil
1398
+ dash.description #=> nil
1399
+
1400
+ dash.description = 'Only slightly less known is ...'
1401
+ dash.update_attributes!(description: nil)
1402
+ dash.description #=> 'none'
1403
+ ```
1404
+
1405
+ The names `required` and `required?` can themselves be used as properties, for example when modeling a Swagger parameter. Their values are independent of Dash's `required: true` declaration option; `false` is a valid non-nil value even for a required property.
1406
+
1407
+ ```ruby
1408
+ class SwaggerParameter < Hashie::Dash
1409
+ property :name, required: true
1410
+ property :required, default: false
1411
+ property :required?
1412
+ end
1413
+
1414
+ parameter = SwaggerParameter.new(name: 'limit', required: true, required?: false)
1415
+ parameter.required # => true
1416
+ parameter.required? # => false
1417
+ parameter.required = false
1418
+ parameter[:required] # => false
1419
+ SwaggerParameter.required?(:name) # => true
1420
+ SwaggerParameter.required?(:required) # => false
1421
+ ```
1422
+
1423
+ ### Lazy Property Loading
1424
+
1425
+ Include `Hashie::Extensions::Dash::Lazy` after other Dash extensions to load properties on demand. The initializer block receives an input key and runs only when that property is first read. Results are cached, including `nil` and `false`; other properties remain unloaded.
1426
+
1427
+ ```ruby
1428
+ class LazyPerson < Hashie::Trash
1429
+ include Hashie::Extensions::Dash::Coercion
1430
+ include Hashie::Extensions::Dash::Lazy
1431
+
1432
+ property :name, from: :fullName, required: true
1433
+ property :age, coerce: Integer
1434
+ end
1435
+
1436
+ source = { fullName: 'Ada', age: '37' }
1437
+ person = LazyPerson.new do |key|
1438
+ # Fetch this property from an expensive source, such as a remote API.
1439
+ source[key]
1440
+ end
1441
+
1442
+ person.name # => 'Ada' (loads :fullName)
1443
+ person.name # => 'Ada' (cached)
1444
+ person.age # => 37 (loads :age)
1445
+ ```
1446
+
1447
+ The loader can query XML or another source instead of a hash. With Trash, it receives the original input key (`:fullName` above); transformations and coercion run only for the property being read. Nested objects need their own loaders to remain lazy.
1448
+
1449
+ Return `nil` for missing input to use the property's default. Defaults and required-property checks run on access, not construction; conditional requirements may load other properties. Errors propagate without caching the failed load, and circular dependencies raise `ArgumentError`.
1450
+
1451
+ On these classes, the initializer block is a property loader, not a Hash default block. Without a block, initialization remains eager. Initial attributes take precedence over the loader, with canonical keys preferred over translated keys. Assignment overrides a pending load and validates normally.
1452
+
1453
+ Getters, `[]`, `fetch`, and `dig` load individual properties. `to_h`, `to_hash`, and `freeze` load all declared properties. Other Hash operations, including enumeration and inspection, see only loaded entries. Synchronize concurrent access externally.
1454
+
1455
+ ### Validations with ActiveModel
1456
+
1457
+ Include [ActiveModel::Validations](https://api.rubyonrails.org/classes/ActiveModel/Validations.html) in a Dash subclass to validate property values with standard validators or custom `validate` methods. Add `gem 'activemodel'` to your application's Gemfile unless Rails already provides it; ActiveModel is optional and is not a Hashie runtime dependency.
1458
+
1459
+ ```ruby
1460
+ require 'logger'
1461
+ require 'active_model'
1462
+ require 'hashie'
1463
+
1464
+ class Article < Hashie::Dash
1465
+ include ActiveModel::Validations
1466
+
1467
+ property :title, required: true
1468
+
1469
+ validates :title, presence: true, length: { maximum: 15 }
1470
+ end
1471
+
1472
+ article = Article.new(title: 'Hello')
1473
+ article.valid? # => true
1474
+
1475
+ article.title = 'A title that is too long' # assignment succeeds
1476
+ article.valid? # => false
1477
+ article.errors[:title] # => ["is too long (maximum is 15 characters)"]
1478
+
1479
+ article.title = ''
1480
+ article.valid? # => false
1481
+ article.errors[:title] # => ["can't be blank"]
1482
+
1483
+ article.title = 'Short title'
1484
+ article.valid? # => true
1485
+ article.errors.empty? # => true
1486
+ ```
1487
+
1488
+ Call `valid?` explicitly after constructing or changing the Dash. Including `ActiveModel::Validations` does not automatically validate on initialization or assignment, reject invalid values, or roll back changes; `valid?` returns a boolean and populates `errors`.
1489
+
1490
+ Dash's `required: true` is a separate non-nil constraint: omitting `title` during initialization or assigning `nil` raises `ArgumentError` immediately, without calling `valid?`. It permits an empty string, whereas ActiveModel's `presence` validator reports blank values when `valid?` runs. Leave off `required: true` if missing or nil values should be collected as validation errors instead of raising.
1491
+
1492
+ ### Potential Gotchas
1493
+
1494
+ Because Dashes are subclasses of the built-in Ruby Hash class, older Ruby versions could preserve the Dash subclass when using it as the first double-splat in a hash literal. The additional keys then existed without declared properties, and property access could raise `NoMethodError`. On Ruby 3.3 and newer the resulting object is a plain Hash:
1495
+
1496
+ <!-- hashie:example dash-double-splat -->
832
1497
  ```ruby
833
1498
  class Foo < Hashie::Dash
834
1499
  property :bar
@@ -836,17 +1501,17 @@ end
836
1501
 
837
1502
  foo = Foo.new(bar: 'baz') #=> {:bar=>"baz"}
838
1503
  qux = { **foo, quux: 'corge' } #=> {:bar=> "baz", :quux=>"corge"}
839
- qux.is_a?(Foo) #=> true
840
- qux[:quux]
841
- #=> raise NoMethodError, "The property 'quux' is not defined for Foo."
1504
+ qux.is_a?(Foo) #=> false
1505
+ qux[:quux] #=> 'corge'
842
1506
  qux.key?(:quux) #=> true
843
1507
  ```
844
1508
 
845
- You can work around this problem in two ways:
1509
+ For code that also targets older Rubies, you can avoid preserving the subclass in two ways:
846
1510
 
847
1511
  1. Call `#to_h` on the resulting object to convert it into a Hash.
848
1512
  2. Use the double-splat operator on the Dash as the last argument in the Hash literal. This will cause the resulting object to be a Hash instead of a Dash, thereby circumventing the problem.
849
1513
 
1514
+ <!-- hashie:example dash-double-splat -->
850
1515
  ```ruby
851
1516
  qux = { **foo, quux: 'corge' }.to_h #=> {:bar=> "baz", :quux=>"corge"}
852
1517
  qux.is_a?(Hash) #=> true
@@ -857,16 +1522,9 @@ qux.is_a?(Hash) #=> true
857
1522
  qux[:quux] #=> "corge"
858
1523
  ```
859
1524
 
860
- ### Dash Extension: PropertyTranslation
861
-
862
- The `Hashie::Extensions::Dash::PropertyTranslation` mixin extends a Dash with
863
- the ability to remap keys from a source hash.
864
-
865
- ### Example from inconsistent APIs
1525
+ ### PropertyTranslation
866
1526
 
867
- Property translation is useful when you need to read data from another
868
- application -- such as a Java API -- where the keys are named differently from
869
- Ruby conventions.
1527
+ Include `Hashie::Extensions::Dash::PropertyTranslation` to map incoming keys to declared properties, for example when an API uses different naming conventions:
870
1528
 
871
1529
  ```ruby
872
1530
  class PersonHash < Hashie::Dash
@@ -880,15 +1538,14 @@ end
880
1538
 
881
1539
  person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh')
882
1540
  person[:first_name] #=> 'Michael'
883
- person[:last_name] #=> 'Bleigh
1541
+ person[:last_name] #=> 'Bleigh'
884
1542
  ```
885
1543
 
886
- ### Example using translation lambdas
887
-
888
- You can also use a lambda to translate the value. This is particularly useful
889
- when you want to ensure the type of data you're wrapping.
1544
+ Use a lambda to transform an incoming value:
890
1545
 
891
1546
  ```ruby
1547
+ require 'time'
1548
+
892
1549
  class DataModelHash < Hashie::Dash
893
1550
  include Hashie::Extensions::Dash::PropertyTranslation
894
1551
 
@@ -897,23 +1554,19 @@ class DataModelHash < Hashie::Dash
897
1554
  end
898
1555
 
899
1556
  model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
900
- model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
1557
+ model.id.class #=> Integer
901
1558
  model.created_at.class #=> Time
902
1559
  ```
903
1560
 
904
- ### Mash and Rails 4 Strong Parameters
905
-
906
- To enable compatibility with Rails 4 use the [hashie-forbidden_attributes](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes) gem.
907
-
908
- ### Dash Extension: Coercion.
1561
+ ### Coercion
909
1562
 
910
- If you want to use `Hashie::Extensions::Coercion` together with `Dash` then
911
- you may probably want to use `Hashie::Extensions::Dash::Coercion` instead.
912
- This extension automatically includes `Hashie::Extensions::Coercion`
913
- and also adds a convenient `:coerce` option to `property` so you can define coercion in one line
914
- instead of using `property` and `coerce_key` separate:
1563
+ Use `Hashie::Extensions::Dash::Coercion` to declare coercion rules with `property ..., coerce:`. It includes the general `Coercion` extension and replaces separate `property` and `coerce_key` declarations:
915
1564
 
916
1565
  ```ruby
1566
+ class PostHash < Hashie::Dash
1567
+ property :title
1568
+ end
1569
+
917
1570
  class UserHash < Hashie::Dash
918
1571
  include Hashie::Extensions::Coercion
919
1572
 
@@ -922,40 +1575,71 @@ class UserHash < Hashie::Dash
922
1575
 
923
1576
  coerce_key :posts, Array[PostHash]
924
1577
  end
1578
+
1579
+ UserHash.new(posts: [{ title: 'Hello' }]).posts.first.title # => 'Hello'
925
1580
  ```
926
1581
 
927
1582
  This is the same as:
928
1583
 
929
1584
  ```ruby
1585
+ class PostHash < Hashie::Dash
1586
+ property :title
1587
+ end
1588
+
930
1589
  class UserHash < Hashie::Dash
931
1590
  include Hashie::Extensions::Dash::Coercion
932
1591
 
933
1592
  property :id
934
1593
  property :posts, coerce: Array[PostHash]
935
1594
  end
1595
+
1596
+ UserHash.new(posts: [{ title: 'Hello' }]).posts.first.title # => 'Hello'
1597
+ ```
1598
+
1599
+ ### PredefinedValues
1600
+
1601
+ The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with the ability to accept predefined values on a property.
1602
+
1603
+ ```ruby
1604
+ class UserHash < Hashie::Dash
1605
+ include Hashie::Extensions::Dash::PredefinedValues
1606
+
1607
+ property :gender, values: %i[male female prefer_not_to_say]
1608
+ property :age, values: (0..150)
1609
+ end
1610
+
1611
+ UserHash.new(gender: :male, age: 42).age # => 42
1612
+ begin
1613
+ UserHash.new(age: -1)
1614
+ rescue ArgumentError
1615
+ # Expected: ArgumentError, /value `-1` is not allowed for `age`/
1616
+ end
936
1617
  ```
937
1618
 
938
1619
  ## Trash
939
1620
 
940
- A Trash is a Dash that allows you to translate keys on initialization. It mixes
941
- in the PropertyTranslation mixin by default and is used like so:
1621
+ A Trash is a Dash that allows you to translate keys on initialization. It mixes in the PropertyTranslation mixin by default and is used like so:
942
1622
 
1623
+ <!-- hashie:example trash -->
943
1624
  ```ruby
944
1625
  class Person < Hashie::Trash
945
1626
  property :first_name, from: :firstName
946
1627
  end
947
1628
  ```
948
1629
 
949
- This will automatically translate the <tt>firstName</tt> key to <tt>first_name</tt>
950
- when it is initialized using a hash such as through:
1630
+ This will automatically translate the `firstName` key to `first_name` when it is initialized using a hash such as through:
951
1631
 
1632
+ <!-- hashie:example trash -->
952
1633
  ```ruby
953
- Person.new(firstName: 'Bob')
1634
+ Person.new(firstName: 'Bob').first_name # => 'Bob'
954
1635
  ```
955
1636
 
956
1637
  Trash also supports translations using lambda, this could be useful when dealing with external API's. You can use it in this way:
957
1638
 
1639
+ <!-- hashie:example trash-result -->
958
1640
  ```ruby
1641
+ require 'time'
1642
+
959
1643
  class Result < Hashie::Trash
960
1644
  property :id, transform_with: lambda { |v| v.to_i }
961
1645
  property :created_at, from: :creation_date, with: lambda { |v| Time.parse(v) }
@@ -964,20 +1648,35 @@ end
964
1648
 
965
1649
  this will produce the following
966
1650
 
1651
+ <!-- hashie:example trash-result -->
967
1652
  ```ruby
968
1653
  result = Result.new(id: '123', creation_date: '2012-03-30 17:23:28')
969
- result.id.class # => Integer (Fixnum if you are using Ruby 2.3 or lower)
1654
+ result.id.class # => Integer
970
1655
  result.created_at.class # => Time
971
1656
  ```
972
1657
 
1658
+ Multiple properties can share a source key to flatten a nested hash. Combine indifferent access with `IgnoreUndeclared` to accept string or symbol input keys and ignore unrelated fields:
1659
+
1660
+ ```ruby
1661
+ class FlattenedPerson < Hashie::Trash
1662
+ include Hashie::Extensions::IgnoreUndeclared
1663
+ include Hashie::Extensions::IndifferentAccess
1664
+
1665
+ property :first_name, from: :name, transform_with: ->(name) { name['first'] }
1666
+ property :last_name, from: :name, transform_with: ->(name) { name['last'] }
1667
+ end
1668
+
1669
+ person = FlattenedPerson.new('name' => { 'first' => 'First', 'last' => 'Last' }, 'extra' => true)
1670
+ person.first_name # => 'First'
1671
+ person.last_name # => 'Last'
1672
+ ```
1673
+
973
1674
  ## Clash
974
1675
 
975
1676
  Clash is a Chainable Lazy Hash that allows you to easily construct complex hashes using method notation chaining. This will allow you to use a more action-oriented approach to building options hashes.
976
1677
 
977
1678
  Essentially, a Clash is a generalized way to provide much of the same kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes provide.
978
1679
 
979
- ### Example:
980
-
981
1680
  ```ruby
982
1681
  c = Hashie::Clash.new
983
1682
  c.where(abc: 'def').order(:created_at)
@@ -995,6 +1694,8 @@ c.where(abc: 'def').where(hgi: 123)
995
1694
  c # => { where: { abc: 'def', hgi: 123 } }
996
1695
  ```
997
1696
 
1697
+ Clash supports YAML and Marshal round-trips, preserving stored data, named subclasses, and parent links used by `_end!`. Only deserialize trusted data. YAML requires Ruby object loading enabled (`Psych.unsafe_load` on modern Psych); ordinary safe loading does not permit arbitrary Ruby classes.
1698
+
998
1699
  ## Rash
999
1700
 
1000
1701
  Rash is a Hash whose keys can be Regexps or Ranges, which will map many input keys to a value.
@@ -1003,12 +1704,10 @@ A good use case for the Rash is an URL router for a web framework, where URLs ne
1003
1704
 
1004
1705
  If the Rash's value is a `proc`, the `proc` will be automatically called with the regexp's MatchData (matched groups) as a block argument.
1005
1706
 
1006
- ### Example:
1007
-
1008
1707
  ```ruby
1009
1708
 
1010
1709
  # Mapping names to appropriate greetings
1011
- greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
1710
+ greeting = Hashie::Rash.new( /^Mr\./ => "Hello sir!", /^Mrs\./ => "Evening, madame." )
1012
1711
  greeting["Mr. Steve Austin"] # => "Hello sir!"
1013
1712
  greeting["Mrs. Steve Austin"] # => "Evening, madame."
1014
1713
 
@@ -1021,18 +1720,34 @@ mapper["I like traffic lights"] # => "Who DOESN'T like traffic lights?!"
1021
1720
  mapper["Get off my lawn!"] # => "Forget your lawn, old man!"
1022
1721
  ```
1023
1722
 
1024
- ### Auto-optimized
1723
+ ### Auto-Optimized
1025
1724
 
1026
1725
  **Note:** The Rash is automatically optimized every 500 accesses (which means that it sorts the list of Regexps, putting the most frequently matched ones at the beginning).
1027
1726
 
1028
1727
  If this value is too low or too high for your needs, you can tune it by setting: `rash.optimize_every = n`.
1029
1728
 
1729
+ ## Logging
1730
+
1731
+ Hashie logs to `STDOUT` by default. Assign `Hashie.logger` to use your application's logger:
1732
+
1733
+ ```ruby
1734
+ require 'logger'
1735
+ require 'hashie'
1736
+
1737
+ Hashie.logger = Logger.new($stdout)
1738
+ ```
1739
+
1740
+ In Rails, use `Hashie.logger = Rails.logger`; Hashie's Railtie sets this automatically during application initialization.
1741
+
1742
+ ## Mascot
1743
+ [![eierlegende Wollmilchsau](./mascot.svg)](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau) Meet Hashie's "official" mascot, the [eierlegende Wollmilchsau](https://en.wiktionary.org/wiki/eierlegende_Wollmilchsau)!
1744
+
1030
1745
  ## Contributing
1031
1746
 
1032
1747
  See [CONTRIBUTING.md](CONTRIBUTING.md)
1033
1748
 
1034
1749
  ## Copyright
1035
1750
 
1036
- Copyright (c) 2009-2014 Intridea, Inc. (http://intridea.com/) and [contributors](https://github.com/hashie/hashie/graphs/contributors).
1751
+ Copyright (c) 2009-2020 [Intridea, Inc.](http://intridea.com), and [contributors](https://github.com/hashie/hashie/graphs/contributors).
1037
1752
 
1038
1753
  MIT License. See [LICENSE](LICENSE) for details.