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
data/README.md CHANGED
@@ -1,26 +1,34 @@
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)
4
+ [![Gem Version](https://img.shields.io/gem/v/hashie.svg)](https://rubygems.org/gems/hashie)
5
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)
6
7
 
7
8
  [![eierlegende Wollmilchsau](./mascot.svg)](#mascot) Hashie is a growing collection of tools that extend Hashes and make them more useful.
8
9
 
9
- # Table of Contents
10
+ ## Table of Contents
10
11
 
11
12
  - [Installation](#installation)
12
13
  - [Stable Release](#stable-release)
13
14
  - [Hash Extensions](#hash-extensions)
14
- - [Logging](#logging)
15
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
- - [A note on circular coercion](#a-note-on-circular-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)
21
26
  - [KeyConversion](#keyconversion)
22
27
  - [MergeInitializer](#mergeinitializer)
28
+ - [DeepMergeInitializer](#deepmergeinitializer)
23
29
  - [MethodAccess](#methodaccess)
30
+ - [BlockAccess](#blockaccess)
31
+ - [Persistable](#persistable)
24
32
  - [MethodAccessWithOverride](#methodaccesswithoverride)
25
33
  - [MethodOverridingInitializer](#methodoverridinginitializer)
26
34
  - [IndifferentAccess](#indifferentaccess)
@@ -28,39 +36,53 @@
28
36
  - [DeepMerge](#deepmerge)
29
37
  - [DeepFetch](#deepfetch)
30
38
  - [DeepFind](#deepfind)
39
+ - [DeepGrep](#deepgrep)
31
40
  - [DeepLocate](#deeplocate)
32
- - [StrictKeyAccess](#strictkeyaccess)
41
+ - [StrictKeyAccess](#strictkeyaccess)
33
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)
34
48
  - [KeepOriginalKeys](#keeporiginalkeys)
35
49
  - [PermissiveRespondTo](#permissiverespondto)
36
50
  - [SafeAssignment](#safeassignment)
51
+ - [TransformKeys](#transformkeys)
52
+ - [UnderscoreKeys](#underscorekeys)
37
53
  - [SymbolizeKeys](#symbolizekeys)
38
54
  - [DefineAccessors](#defineaccessors)
55
+ - [StrictKeyAccess](#strictkeyaccess-1)
56
+ - [RequiredKeys](#requiredkeys)
39
57
  - [Dash](#dash)
58
+ - [Lazy Property Loading](#lazy-property-loading)
59
+ - [Validations with ActiveModel](#validations-with-activemodel)
40
60
  - [Potential Gotchas](#potential-gotchas)
41
61
  - [PropertyTranslation](#propertytranslation)
42
- - [Mash and Rails 4 Strong Parameters](#mash-and-rails-4-strong-parameters)
43
62
  - [Coercion](#coercion-1)
44
63
  - [PredefinedValues](#predefinedvalues)
45
64
  - [Trash](#trash)
46
65
  - [Clash](#clash)
47
66
  - [Rash](#rash)
48
67
  - [Auto-Optimized](#auto-optimized)
68
+ - [Logging](#logging)
49
69
  - [Mascot](#mascot)
50
70
  - [Contributing](#contributing)
51
71
  - [Copyright](#copyright)
52
72
 
53
73
  ## Installation
54
74
 
75
+ Hashie requires Ruby 3.0 or newer.
76
+
55
77
  Hashie is available as a RubyGem:
56
78
 
57
79
  ```bash
58
- $ gem install hashie
80
+ gem install hashie
59
81
  ```
60
82
 
61
83
  ## Stable Release
62
84
 
63
- You're reading the documentation for the stable release of Hashie, 5.1.0.
85
+ You're reading the documentation for the stable release of Hashie, 5.2.0.
64
86
 
65
87
  ## Hash Extensions
66
88
 
@@ -68,20 +90,16 @@ The library is broken up into a number of atomically includable Hash extension m
68
90
 
69
91
  Any of the extensions listed below can be mixed into a class by `include`-ing `Hashie::Extensions::ExtensionName`.
70
92
 
71
- ## Logging
72
-
73
- 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:
74
-
75
- ```ruby
76
- # Set the logger to the Rails logger
77
- Hashie.logger = Rails.logger
78
- ```
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.
79
94
 
80
95
  ### Coercion
81
96
 
82
- 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:
83
98
 
84
99
  ```ruby
100
+ class User < Hashie::Mash
101
+ end
102
+
85
103
  class Tweet < Hash
86
104
  include Hashie::Extensions::Coercion
87
105
  include Hashie::Extensions::MergeInitializer
@@ -89,9 +107,10 @@ class Tweet < Hash
89
107
  end
90
108
 
91
109
  user_hash = { name: "Bob" }
92
- Tweet.new(user: user_hash)
93
- # => automatically calls User.coerce(user_hash) or
94
- # 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"
95
114
  ```
96
115
 
97
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.
@@ -108,31 +127,45 @@ class SpecialHash < Hash
108
127
  end
109
128
  end
110
129
  end
130
+
131
+ hash = SpecialHash.new(child: { name: 'Bob' })
132
+ hash[:child].class # => SpecialHash
133
+ hash[:child][:name] # => 'Bob'
111
134
  ```
112
135
 
113
- ### Coercing Collections
136
+ #### Coercing Collections
114
137
 
115
138
  ```ruby
139
+ require 'set'
140
+
141
+ class User < Hashie::Mash
142
+ end
143
+
116
144
  class Tweet < Hash
117
145
  include Hashie::Extensions::Coercion
146
+ include Hashie::Extensions::MergeInitializer
147
+ include Hashie::Extensions::MethodAccess
118
148
  coerce_key :mentions, Array[User]
119
149
  coerce_key :friends, Set[User]
120
150
  end
121
151
 
122
152
  user_hash = { name: "Bob" }
123
- mentions_hash= [user_hash, user_hash]
153
+ mentions_hash = [user_hash, user_hash]
124
154
  friends_hash = [user_hash]
125
155
  tweet = Tweet.new(mentions: mentions_hash, friends: friends_hash)
126
- # => automatically calls User.coerce(user_hash) or
127
- # 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.
128
158
 
129
159
  tweet.mentions.map(&:class) # => [User, User]
130
160
  tweet.friends.class # => Set
131
161
  ```
132
162
 
133
- ### Coercing Hashes
163
+ #### Coercing Hashes
134
164
 
135
165
  ```ruby
166
+ class User < Hashie::Mash
167
+ end
168
+
136
169
  class Relation
137
170
  def initialize(string)
138
171
  @relation = string
@@ -141,20 +174,22 @@ end
141
174
 
142
175
  class Tweet < Hash
143
176
  include Hashie::Extensions::Coercion
177
+ include Hashie::Extensions::MergeInitializer
178
+ include Hashie::Extensions::MethodAccess
144
179
  coerce_key :relations, Hash[User => Relation]
145
180
  end
146
181
 
147
182
  user_hash = { name: "Bob" }
148
- relations_hash= { user_hash => "father", user_hash => "friend" }
183
+ relations_hash = { user_hash => "father", { name: "Alice" } => "friend" }
149
184
  tweet = Tweet.new(relations: relations_hash)
150
185
  tweet.relations.map { |k,v| [k.class, v.class] } # => [[User, Relation], [User, Relation]]
151
186
  tweet.relations.class # => Hash
152
187
 
153
- # => automatically calls User.coerce(user_hash) on each key
154
- # 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`.
155
190
  ```
156
191
 
157
- ### Coercing Core Types
192
+ #### Coercing Core Types
158
193
 
159
194
  Hashie handles coercion to the following by using standard conversion methods:
160
195
 
@@ -169,19 +204,18 @@ Hashie handles coercion to the following by using standard conversion methods:
169
204
 
170
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.
171
206
 
172
- You can also use coerce from the following supertypes with `coerce_value`:
173
- - Integer
174
- - Numeric
207
+ `coerce_value` also supports the supertypes `Integer` and `Numeric`.
175
208
 
176
- 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.
177
210
 
178
- ### Coercion Proc
211
+ #### Coercion Proc
179
212
 
180
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:
181
214
 
182
215
  ```ruby
183
216
  class Tweet < Hash
184
217
  include Hashie::Extensions::Coercion
218
+ include Hashie::Extensions::MergeInitializer
185
219
  coerce_key :retweeted, ->(v) do
186
220
  case v
187
221
  when String
@@ -193,43 +227,199 @@ class Tweet < Hash
193
227
  end
194
228
  end
195
229
  end
230
+
231
+ Tweet.new(retweeted: 'yes')[:retweeted] # => true
232
+ Tweet.new(retweeted: 'false')[:retweeted] # => false
196
233
  ```
197
234
 
198
- #### A note on circular coercion
235
+ ##### Circular Coercion
199
236
 
200
237
  Since `coerce_key` is a class-level method, you cannot have circular coercion without the use of a proc. For example:
201
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
+
202
260
  ```ruby
203
261
  class CategoryHash < Hash
204
262
  include Hashie::Extensions::Coercion
205
263
  include Hashie::Extensions::MergeInitializer
206
264
 
207
- 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
208
270
  end
209
271
 
210
272
  class ProductHash < Hash
211
273
  include Hashie::Extensions::Coercion
212
274
  include Hashie::Extensions::MergeInitializer
213
275
 
214
- coerce_key :categories, Array[CategoriesHash]
276
+ coerce_key :categories, Array[CategoryHash]
215
277
  end
278
+
279
+ category = CategoryHash.new(products: [{ categories: [{}] }])
280
+ category[:products].first.class # => ProductHash
281
+ category[:products].first[:categories].first.class # => CategoryHash
216
282
  ```
217
283
 
218
- 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
219
285
 
220
- 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:
221
287
 
222
288
  ```ruby
223
- class CategoryHash < Hash
224
- # ...
225
- coerce_key :products, ->(value) do
226
- return value.map { |v| ProductHash.new(v) } if value.respond_to?(:map)
289
+ class DescriptionParams < Hashie::Dash
290
+ include Hashie::Extensions::Coercion
227
291
 
228
- ProductHash.new(value)
229
- end
292
+ property :description, default: ''
293
+ coerce_key :description, String
230
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 # => ''
231
305
  ```
232
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)
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
419
+ ```
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
+
233
423
  ### KeyConversion
234
424
 
235
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`.
@@ -237,16 +427,51 @@ The KeyConversion extension gives you the convenience methods of `symbolize_keys
237
427
  Hashie also has a utility method for converting keys on a Hash without a mixin:
238
428
 
239
429
  ```ruby
240
- Hashie.symbolize_keys! hash # => Symbolizes all string keys of hash.
241
- Hashie.symbolize_keys hash # => Returns a copy of hash with string keys symbolized.
242
- Hashie.stringify_keys! hash # => Stringifies keys of hash.
243
- 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' }
244
437
  ```
245
438
 
246
439
  ### MergeInitializer
247
440
 
248
441
  The MergeInitializer extension simply makes it possible to initialize a Hash subclass with another Hash, giving you a quick short-hand.
249
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
+
250
475
  ### MethodAccess
251
476
 
252
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`.
@@ -262,6 +487,56 @@ h.abc # => 'def'
262
487
  h.abc? # => true
263
488
  ```
264
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
+
265
540
  ### MethodAccessWithOverride
266
541
 
267
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`.
@@ -311,6 +586,7 @@ This extension can be mixed in to your Hash subclass to allow you to use Strings
311
586
 
312
587
  In addition, IndifferentAccess will also inject itself into sub-hashes so they behave the same.
313
588
 
589
+ <!-- hashie:example indifferent-access -->
314
590
  ```ruby
315
591
  class MyHash < Hash
316
592
  include Hashie::Extensions::MergeInitializer
@@ -332,14 +608,15 @@ myhash['fishes']['food'] # => "flakes"
332
608
 
333
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:
334
610
 
611
+ <!-- hashie:example indifferent-access -->
335
612
  ```ruby
336
613
  myhash = MyHash.new
337
614
  myhash["foo"] = "bar"
338
615
  myhash[:foo] #=> "bar"
339
616
 
340
617
  normal_hash = myhash.to_hash
341
- myhash["foo"] #=> "bar"
342
- myhash[:foo] #=> nil
618
+ normal_hash["foo"] #=> "bar"
619
+ normal_hash[:foo] #=> nil
343
620
  ```
344
621
 
345
622
  ### IgnoreUndeclared
@@ -347,7 +624,7 @@ myhash[:foo] #=> nil
347
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.
348
625
 
349
626
  ```ruby
350
- class Person < Trash
627
+ class Person < Hashie::Trash
351
628
  include Hashie::Extensions::IgnoreUndeclared
352
629
  property :first_name
353
630
  property :last_name
@@ -363,13 +640,16 @@ p = Person.new(user_data) # 'email' is silently ignored
363
640
 
364
641
  p.first_name # => 'Freddy'
365
642
  p.last_name # => 'Nostrils'
366
- p.email # => NoMethodError
643
+ begin
644
+ p.email
645
+ rescue NoMethodError
646
+ # Expected: NoMethodError
647
+ end
367
648
  ```
368
649
 
369
650
  ### DeepMerge
370
651
 
371
- This extension allows you to easily include a recursive merging
372
- system into any Hash descendant:
652
+ Include `DeepMerge` to merge nested hashes recursively:
373
653
 
374
654
  ```ruby
375
655
  class MyHash < Hash
@@ -393,11 +673,9 @@ end
393
673
  h1 = MyHash[{ a: 100, b: 200, c: { c1: 100 } }]
394
674
  h2 = MyHash[{ b: 250, c: { c1: 200 } }]
395
675
 
396
- h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
397
- # => { 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 } }
398
677
  ```
399
678
 
400
-
401
679
  ### DeepFetch
402
680
 
403
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.
@@ -416,7 +694,11 @@ user = {
416
694
  user.extend Hashie::Extensions::DeepFetch
417
695
 
418
696
  user.deep_fetch :name, :first # => 'Bob'
419
- 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
420
702
 
421
703
  # using a default block
422
704
  user.deep_fetch(:name, :middle) { |key| 'default' } # => 'default'
@@ -451,11 +733,39 @@ user.deep_find_all(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'O
451
733
  user.deep_select(:name) #=> [{ first: 'Bob', last: 'Boberts' }, 'Rubyists', 'Open source enthusiasts']
452
734
  ```
453
735
 
454
- ### DeepLocate
736
+ ### DeepGrep
737
+
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)`.
739
+
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
+ ]
455
752
 
456
- This extension can be mixed in to provide a depth first search based search for enumerables matching a given comparator callable.
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.
457
765
 
458
- It returns all enumerables which contain at least one element, for which the given comparator returns ```true```.
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.
459
769
 
460
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.
461
771
 
@@ -483,17 +793,14 @@ books = [
483
793
 
484
794
  books.extend(Hashie::Extensions::DeepLocate)
485
795
 
486
- # for ruby 1.9 leave *no* space between the lambda rocket and the braces
487
- # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
488
-
489
- books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
490
- # => [{: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}]
491
797
 
492
- books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
493
- # => [{: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}]
494
799
  ```
495
800
 
496
- ## 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.
802
+
803
+ ### StrictKeyAccess
497
804
 
498
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.
499
806
 
@@ -502,17 +809,18 @@ class StrictKeyAccessHash < Hash
502
809
  include Hashie::Extensions::StrictKeyAccess
503
810
  end
504
811
 
505
- >> hash = StrictKeyAccessHash[foo: "bar"]
506
- => {:foo=>"bar"}
507
- >> hash[:foo]
508
- => "bar"
509
- >> hash[:cow]
510
- 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
511
819
  ```
512
820
 
513
821
  ## Mash
514
822
 
515
- 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.
823
+ Mash adds method-style access to Hash keys and recursively wraps nested hashes:
516
824
 
517
825
  ```ruby
518
826
  mash = Hashie::Mash.new
@@ -521,37 +829,34 @@ mash.name # => nil
521
829
  mash.name = "My Mash"
522
830
  mash.name # => "My Mash"
523
831
  mash.name? # => true
524
- mash.inspect # => <Hashie::Mash name="My Mash">
832
+ mash.inspect # => '#<Hashie::Mash name="My Mash">'
525
833
 
526
834
  mash = Hashie::Mash.new
527
835
  # use bang methods for multi-level assignment
528
836
  mash.author!.name = "Michael Bleigh"
529
- mash.author # => <Hashie::Mash name="Michael Bleigh">
837
+ mash.author.to_h # => { 'name' => 'Michael Bleigh' }
530
838
 
531
839
  mash = Hashie::Mash.new
532
840
  # use under-bang methods for multi-level testing
533
841
  mash.author_.name? # => false
534
- mash.inspect # => <Hashie::Mash>
842
+ mash.inspect # => '#<Hashie::Mash>'
535
843
  ```
536
844
 
537
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.
538
846
 
539
- _How does Mash handle conflicts with pre-existing methods?_
847
+ ### Method Conflicts
540
848
 
541
- 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.
849
+ Mash does not override existing methods. Use brackets to read a key that conflicts with a method name:
542
850
 
543
851
  ```ruby
544
852
  mash = Hashie::Mash.new
545
853
  mash.name = "My Mash"
546
854
  mash.zip = "Method Override?"
547
855
  mash.zip # => [[["name", "My Mash"]], [["zip", "Method Override?"]]]
856
+ mash[:zip] # => 'Method Override?'
548
857
  ```
549
858
 
550
- 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:
551
-
552
- ```ruby
553
- Hashie.logger = Rails.logger
554
- ```
859
+ Hashie logs a warning when a key conflicts with an existing method. See [Logging](#logging) to configure the logger.
555
860
 
556
861
  You can also disable the logging in subclasses of Mash:
557
862
 
@@ -559,14 +864,19 @@ You can also disable the logging in subclasses of Mash:
559
864
  class Response < Hashie::Mash
560
865
  disable_warnings
561
866
  end
867
+
868
+ Response.new(zip: '90210').zip # => [[['zip', '90210']]]
869
+ Response.disable_warnings? # => true
562
870
  ```
563
871
 
564
- 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`:
565
873
 
566
874
  ```ruby
567
875
  class Response < Hashie::Mash
568
876
  disable_warnings :zip, :zap
569
877
  end
878
+
879
+ Response.disabled_warnings # => [:zip, :zap]
570
880
  ```
571
881
 
572
882
  This behavior is cumulative. The examples above and below behave identically.
@@ -576,21 +886,29 @@ class Response < Hashie::Mash
576
886
  disable_warnings :zip
577
887
  disable_warnings :zap
578
888
  end
889
+
890
+ Response.disabled_warnings # => [:zip, :zap]
579
891
  ```
580
892
 
581
- 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:
582
894
 
583
895
  ```ruby
896
+ require 'stringio'
897
+
584
898
  class Message < Hashie::Mash
585
899
  disable_warnings :zip, :zap
586
900
  disable_warnings
587
901
  end
588
902
 
589
- # No errors will be logged
903
+ log = StringIO.new
904
+ Hashie.logger = Logger.new(log)
590
905
  Message.new(merge: 'true', compact: true)
906
+ log.string # => ''
591
907
  ```
592
908
 
593
909
  ```ruby
910
+ require 'stringio'
911
+
594
912
  class Message < Hashie::Mash
595
913
  disable_warnings
596
914
  end
@@ -599,20 +917,31 @@ class Response < Message
599
917
  disable_warnings :zip, :zap
600
918
  end
601
919
 
602
- # 2 errors will be logged
920
+ log = StringIO.new
921
+ Hashie.logger = Logger.new(log)
603
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
604
926
  ```
605
927
 
606
928
  If you would like to create an anonymous subclass of a Hashie::Mash with key conflict warnings disabled:
607
929
 
608
930
  ```ruby
609
- Hashie::Mash.quiet.new(zip: '90210', compact: true) # no errors logged
610
- 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
611
940
  ```
612
941
 
613
- _How does the wrapping of Mash sub-Hashes work?_
942
+ ### Nested Hashes
614
943
 
615
- 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.
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:
616
945
 
617
946
  ```ruby
618
947
  mash = Hashie::Mash.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0" })
@@ -623,75 +952,127 @@ my_gem = MyGem.new(name: "Hashie", dependencies: { rake: "< 11", rspec: "~> 3.0"
623
952
  my_gem.dependencies.class #=> MyGem
624
953
  ```
625
954
 
626
- _How does Mash handle key types which cannot be symbolized?_
955
+ ### Key Types
627
956
 
628
- Mash preserves keys which cannot be converted *directly* to both a string and a symbol, such as numeric keys. Since Mash is conceived to provide pseudo-object functionality, handling keys which cannot represent a method call falls outside its scope of value.
957
+ Mash uses string keys for indifferent string/symbol access. Other key types, such as integers, are preserved and accessed with brackets:
629
958
 
630
959
  ```ruby
631
- Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num')
632
- # => {"1"=>"one sym", 1=>"one num"}
960
+ Hashie::Mash.new('1' => 'one string', :'1' => 'one sym', 1 => 'one num') # => {"1"=>"one sym", 1=>"one num"}
633
961
  ```
634
962
 
635
- The symbol key `:'1'` is converted the string `'1'` to support indifferent access and consequently its value `'one sym'` will override the previously set `'one string'`. However, the subsequent key of `1` cannot directly convert to a symbol and therefore **not** converted to the string `'1'` that would otherwise override the previously set value of `'one sym'`.
963
+ The symbol `:'1'` and string `'1'` share a key, so the later value wins. The integer `1` remains a separate key.
636
964
 
637
- _What else can Mash do?_
965
+ ### Loading Files
638
966
 
639
- Mash allows you also to transform any files into a Mash objects.
967
+ Use `Mash.load` to read a YAML configuration file:
640
968
 
641
969
  ```yml
642
- #/etc/config/settings/twitter.yml
970
+ # settings/twitter.yml
643
971
  development:
644
972
  api_key: 'api_key'
645
973
  production:
646
- 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
647
975
  ```
648
976
 
977
+ <!-- hashie:example mash-load -->
649
978
  ```ruby
650
- mash = Mash.load('settings/twitter.yml')
651
- mash.development.api_key # => 'localhost'
652
- 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
653
998
  mash.development.api_key? # => true
999
+ mash.development.api_key = "foo"
1000
+ mash.development.api_key # => 'foo'
654
1001
  ```
655
1002
 
656
1003
  You can also load with a `Pathname` object:
657
1004
 
1005
+ <!-- hashie:example mash-load -->
658
1006
  ```ruby
659
- mash = Mash.load(Pathname 'settings/twitter.yml')
660
- mash.development.api_key # => 'localhost'
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
+
1015
+ ```ruby
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 %>'
661
1019
  ```
662
1020
 
663
1021
  You can access a Mash from another class:
664
1022
 
1023
+ <!-- hashie:example mash-load -->
665
1024
  ```ruby
666
- mash = Mash.load('settings/twitter.yml')[ENV['RACK_ENV']]
1025
+ class Twitter
1026
+ end
1027
+
1028
+ mash = Hashie::Mash.load('settings/twitter.yml')['production']
667
1029
  Twitter.extend mash.to_module # NOTE: if you want another name than settings, call: to_module('my_settings')
668
1030
  Twitter.settings.api_key # => 'abcd'
1031
+ ENV.delete('HASHIE_README_API_KEY')
669
1032
  ```
670
1033
 
671
- 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):
672
1035
 
673
- ```
674
- #/etc/data/user.csv
675
- id | name | lastname
676
- ---|------------- | -------------
677
- 1 |John | Doe
678
- 2 |Laurent | Garnier
1036
+ ```text
1037
+ # data/user.csv
1038
+ id,name,lastname
1039
+ 1,John,Doe
1040
+ 2,Laurent,Garnier
679
1041
  ```
680
1042
 
681
1043
  ```ruby
682
- mash = Mash.load('data/user.csv', parser: MyCustomCsvParser)
683
- # => { 1 => { name: 'John', lastname: 'Doe'}, 2 => { name: 'Laurent', lastname: 'Garnier' } }
684
- 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' }
685
1060
  ```
686
1061
 
687
- The `Mash#load` method calls `YAML.safe_load(path, [], [], true)`.
1062
+ Both built-in YAML parsers use `YAML.safe_load`.
688
1063
 
689
1064
  Specify `permitted_symbols`, `permitted_classes` and `aliases` options as needed.
690
1065
 
691
1066
  ```ruby
692
- 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'
693
1070
  ```
694
1071
 
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.
1075
+
695
1076
  ### KeepOriginalKeys
696
1077
 
697
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.
@@ -733,7 +1114,7 @@ mash = PermissiveMash.new(a: 1)
733
1114
  mash.respond_to? :b #=> true
734
1115
  ```
735
1116
 
736
- 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.
737
1118
 
738
1119
  ### SafeAssignment
739
1120
 
@@ -745,14 +1126,58 @@ class SafeMash < ::Hashie::Mash
745
1126
  end
746
1127
 
747
1128
  safe_mash = SafeMash.new
748
- safe_mash.zip = 'Test' # => ArgumentError
749
- 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
750
1139
  ```
751
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' }
1172
+ ```
1173
+
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.
1175
+
752
1176
  ### SymbolizeKeys
753
1177
 
754
- 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 string keys to symbols. It can be useful to use with keywords argument, which required symbol keys.
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:
755
1179
 
1180
+ <!-- hashie:example symbolized-mash -->
756
1181
  ```ruby
757
1182
  class SymbolizedMash < ::Hashie::Mash
758
1183
  include Hashie::Extensions::Mash::SymbolizeKeys
@@ -764,27 +1189,27 @@ symbol_mash.test #=> 'value'
764
1189
  symbol_mash.to_h #=> {test: 'value'}
765
1190
 
766
1191
  def example(test:)
767
- puts test
1192
+ test
768
1193
  end
769
1194
 
770
- example(symbol_mash) #=> value
1195
+ example(**symbol_mash) #=> 'value'
771
1196
  ```
772
1197
 
773
- 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:
774
1199
 
1200
+ <!-- hashie:example symbolized-mash -->
775
1201
  ```ruby
776
1202
  symbol_mash = SymbolizedMash.new(id: 123, name: 'Rey')
1203
+ entries = []
777
1204
  symbol_mash.each do |key, value|
778
- # key is :id, then :name
779
- # value is 123, then 'Rey'
1205
+ entries << [key, value]
780
1206
  end
1207
+ entries # => [[:id, 123], [:name, 'Rey']]
781
1208
  ```
782
1209
 
783
- 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.
784
-
785
1210
  ### DefineAccessors
786
1211
 
787
- 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`.
788
1213
 
789
1214
  ```ruby
790
1215
  class MyHash < ::Hashie::Mash
@@ -805,15 +1230,76 @@ You can also extend the existing mash without defining a class:
805
1230
 
806
1231
  ```ruby
807
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
808
1288
  ```
809
1289
 
810
1290
  ## Dash
811
1291
 
812
- 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. A property with a proc value will be evaluated lazily upon retrieval.
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.
813
1293
 
814
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.
815
1295
 
816
1296
  ```ruby
1297
+ class Genome
1298
+ def self.sequence
1299
+ 'GATTACA'
1300
+ end
1301
+ end
1302
+
817
1303
  class Person < Hashie::Dash
818
1304
  property :name, required: true
819
1305
  property :age, required: true, message: 'must be set.'
@@ -828,18 +1314,38 @@ class Person < Hashie::Dash
828
1314
  end
829
1315
  end
830
1316
 
831
- p = Person.new # => ArgumentError: The property 'name' is required for this Dash.
832
- 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
833
1327
 
834
- p = Person.new(name: "Bob", age: 18)
1328
+ p = Person.new(name: "Bob", age: 18, email: 'bob@example.com', pants: true)
835
1329
  p.name # => 'Bob'
836
- 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
837
1335
  p.age # => 18
838
- 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
839
1341
  p.email = 'abc@def.com'
840
1342
  p.occupation # => 'Rubyist'
841
1343
  p.email # => 'abc@def.com'
842
- p[:awesome] # => NoMethodError
1344
+ begin
1345
+ p[:awesome]
1346
+ rescue NoMethodError
1347
+ # Expected: NoMethodError
1348
+ end
843
1349
  p[:occupation] # => 'Rubyist'
844
1350
  p.update_attributes!(name: 'Trudy', occupation: 'Evil')
845
1351
  p.occupation # => 'Evil'
@@ -859,12 +1365,12 @@ class Tricky < Hashie::Dash
859
1365
  end
860
1366
 
861
1367
  p = Tricky.new(trick: 'one', 'trick' => 'two')
862
- p.trick # => 'one', always symbol version
1368
+ p.trick # => 'two'
863
1369
  p[:trick] # => 'one'
864
1370
  p['trick'] # => 'two'
865
1371
  ```
866
1372
 
867
- 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.
868
1374
 
869
1375
  ```ruby
870
1376
  class Tricky < Hashie::Dash
@@ -872,7 +1378,7 @@ class Tricky < Hashie::Dash
872
1378
  end
873
1379
 
874
1380
  p = Tricky.new('trick' => 'two')
875
- p.trick # => NoMethodError
1381
+ p.trick # => 'two'
876
1382
  ```
877
1383
 
878
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!`.
@@ -896,10 +1402,98 @@ dash.update_attributes!(description: nil)
896
1402
  dash.description #=> 'none'
897
1403
  ```
898
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
+
899
1492
  ### Potential Gotchas
900
1493
 
901
- 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:
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:
902
1495
 
1496
+ <!-- hashie:example dash-double-splat -->
903
1497
  ```ruby
904
1498
  class Foo < Hashie::Dash
905
1499
  property :bar
@@ -907,17 +1501,17 @@ end
907
1501
 
908
1502
  foo = Foo.new(bar: 'baz') #=> {:bar=>"baz"}
909
1503
  qux = { **foo, quux: 'corge' } #=> {:bar=> "baz", :quux=>"corge"}
910
- qux.is_a?(Foo) #=> true
911
- qux[:quux]
912
- #=> raise NoMethodError, "The property 'quux' is not defined for Foo."
1504
+ qux.is_a?(Foo) #=> false
1505
+ qux[:quux] #=> 'corge'
913
1506
  qux.key?(:quux) #=> true
914
1507
  ```
915
1508
 
916
- 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:
917
1510
 
918
1511
  1. Call `#to_h` on the resulting object to convert it into a Hash.
919
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.
920
1513
 
1514
+ <!-- hashie:example dash-double-splat -->
921
1515
  ```ruby
922
1516
  qux = { **foo, quux: 'corge' }.to_h #=> {:bar=> "baz", :quux=>"corge"}
923
1517
  qux.is_a?(Hash) #=> true
@@ -930,12 +1524,7 @@ qux[:quux] #=> "corge"
930
1524
 
931
1525
  ### PropertyTranslation
932
1526
 
933
- The `Hashie::Extensions::Dash::PropertyTranslation` mixin extends a Dash with
934
- the ability to remap keys from a source hash.
935
-
936
- Property translation is useful when you need to read data from another
937
- application -- such as a Java API -- where the keys are named differently from
938
- Ruby conventions.
1527
+ Include `Hashie::Extensions::Dash::PropertyTranslation` to map incoming keys to declared properties, for example when an API uses different naming conventions:
939
1528
 
940
1529
  ```ruby
941
1530
  class PersonHash < Hashie::Dash
@@ -949,13 +1538,14 @@ end
949
1538
 
950
1539
  person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh')
951
1540
  person[:first_name] #=> 'Michael'
952
- person[:last_name] #=> 'Bleigh
1541
+ person[:last_name] #=> 'Bleigh'
953
1542
  ```
954
1543
 
955
- You can also use a lambda to translate the value. This is particularly useful
956
- when you want to ensure the type of data you're wrapping.
1544
+ Use a lambda to transform an incoming value:
957
1545
 
958
1546
  ```ruby
1547
+ require 'time'
1548
+
959
1549
  class DataModelHash < Hashie::Dash
960
1550
  include Hashie::Extensions::Dash::PropertyTranslation
961
1551
 
@@ -964,23 +1554,19 @@ class DataModelHash < Hashie::Dash
964
1554
  end
965
1555
 
966
1556
  model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
967
- model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
1557
+ model.id.class #=> Integer
968
1558
  model.created_at.class #=> Time
969
1559
  ```
970
1560
 
971
- ### Mash and Rails 4 Strong Parameters
972
-
973
- To enable compatibility with Rails 4 use the [hashie-forbidden_attributes](https://github.com/Maxim-Filimonov/hashie-forbidden_attributes) gem.
974
-
975
1561
  ### Coercion
976
1562
 
977
- If you want to use `Hashie::Extensions::Coercion` together with `Dash` then
978
- you may probably want to use `Hashie::Extensions::Dash::Coercion` instead.
979
- This extension automatically includes `Hashie::Extensions::Coercion`
980
- and also adds a convenient `:coerce` option to `property` so you can define coercion in one line
981
- 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:
982
1564
 
983
1565
  ```ruby
1566
+ class PostHash < Hashie::Dash
1567
+ property :title
1568
+ end
1569
+
984
1570
  class UserHash < Hashie::Dash
985
1571
  include Hashie::Extensions::Coercion
986
1572
 
@@ -989,23 +1575,30 @@ class UserHash < Hashie::Dash
989
1575
 
990
1576
  coerce_key :posts, Array[PostHash]
991
1577
  end
1578
+
1579
+ UserHash.new(posts: [{ title: 'Hello' }]).posts.first.title # => 'Hello'
992
1580
  ```
993
1581
 
994
1582
  This is the same as:
995
1583
 
996
1584
  ```ruby
1585
+ class PostHash < Hashie::Dash
1586
+ property :title
1587
+ end
1588
+
997
1589
  class UserHash < Hashie::Dash
998
1590
  include Hashie::Extensions::Dash::Coercion
999
1591
 
1000
1592
  property :id
1001
1593
  property :posts, coerce: Array[PostHash]
1002
1594
  end
1595
+
1596
+ UserHash.new(posts: [{ title: 'Hello' }]).posts.first.title # => 'Hello'
1003
1597
  ```
1004
1598
 
1005
1599
  ### PredefinedValues
1006
1600
 
1007
- The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with
1008
- the ability to accept predefined values on a property.
1601
+ The `Hashie::Extensions::Dash::PredefinedValues` mixin extends a Dash with the ability to accept predefined values on a property.
1009
1602
 
1010
1603
  ```ruby
1011
1604
  class UserHash < Hashie::Dash
@@ -1014,29 +1607,39 @@ class UserHash < Hashie::Dash
1014
1607
  property :gender, values: %i[male female prefer_not_to_say]
1015
1608
  property :age, values: (0..150)
1016
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
1017
1617
  ```
1018
1618
 
1019
1619
  ## Trash
1020
1620
 
1021
- A Trash is a Dash that allows you to translate keys on initialization. It mixes
1022
- 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:
1023
1622
 
1623
+ <!-- hashie:example trash -->
1024
1624
  ```ruby
1025
1625
  class Person < Hashie::Trash
1026
1626
  property :first_name, from: :firstName
1027
1627
  end
1028
1628
  ```
1029
1629
 
1030
- This will automatically translate the <tt>firstName</tt> key to <tt>first_name</tt>
1031
- 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:
1032
1631
 
1632
+ <!-- hashie:example trash -->
1033
1633
  ```ruby
1034
- Person.new(firstName: 'Bob')
1634
+ Person.new(firstName: 'Bob').first_name # => 'Bob'
1035
1635
  ```
1036
1636
 
1037
1637
  Trash also supports translations using lambda, this could be useful when dealing with external API's. You can use it in this way:
1038
1638
 
1639
+ <!-- hashie:example trash-result -->
1039
1640
  ```ruby
1641
+ require 'time'
1642
+
1040
1643
  class Result < Hashie::Trash
1041
1644
  property :id, transform_with: lambda { |v| v.to_i }
1042
1645
  property :created_at, from: :creation_date, with: lambda { |v| Time.parse(v) }
@@ -1045,12 +1648,29 @@ end
1045
1648
 
1046
1649
  this will produce the following
1047
1650
 
1651
+ <!-- hashie:example trash-result -->
1048
1652
  ```ruby
1049
1653
  result = Result.new(id: '123', creation_date: '2012-03-30 17:23:28')
1050
- result.id.class # => Integer (Fixnum if you are using Ruby 2.3 or lower)
1654
+ result.id.class # => Integer
1051
1655
  result.created_at.class # => Time
1052
1656
  ```
1053
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
+
1054
1674
  ## Clash
1055
1675
 
1056
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.
@@ -1074,6 +1694,8 @@ c.where(abc: 'def').where(hgi: 123)
1074
1694
  c # => { where: { abc: 'def', hgi: 123 } }
1075
1695
  ```
1076
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
+
1077
1699
  ## Rash
1078
1700
 
1079
1701
  Rash is a Hash whose keys can be Regexps or Ranges, which will map many input keys to a value.
@@ -1085,7 +1707,7 @@ If the Rash's value is a `proc`, the `proc` will be automatically called with th
1085
1707
  ```ruby
1086
1708
 
1087
1709
  # Mapping names to appropriate greetings
1088
- greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
1710
+ greeting = Hashie::Rash.new( /^Mr\./ => "Hello sir!", /^Mrs\./ => "Evening, madame." )
1089
1711
  greeting["Mr. Steve Austin"] # => "Hello sir!"
1090
1712
  greeting["Mrs. Steve Austin"] # => "Evening, madame."
1091
1713
 
@@ -1104,6 +1726,19 @@ mapper["Get off my lawn!"] # => "Forget your lawn, old man!"
1104
1726
 
1105
1727
  If this value is too low or too high for your needs, you can tune it by setting: `rash.optimize_every = n`.
1106
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
+
1107
1742
  ## Mascot
1108
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)!
1109
1744