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