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