hashie 3.5.7 → 5.0.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 +5 -5
- data/CHANGELOG.md +281 -195
- data/CONTRIBUTING.md +13 -6
- data/LICENSE +1 -1
- data/README.md +320 -60
- data/Rakefile +2 -2
- data/UPGRADING.md +121 -7
- data/hashie.gemspec +13 -7
- data/lib/hashie/clash.rb +12 -1
- data/lib/hashie/dash.rb +56 -35
- data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
- data/lib/hashie/extensions/coercion.rb +26 -19
- data/lib/hashie/extensions/dash/indifferent_access.rb +29 -1
- data/lib/hashie/extensions/dash/predefined_values.rb +88 -0
- data/lib/hashie/extensions/dash/property_translation.rb +59 -28
- data/lib/hashie/extensions/deep_fetch.rb +5 -3
- data/lib/hashie/extensions/deep_find.rb +14 -5
- data/lib/hashie/extensions/deep_locate.rb +22 -8
- data/lib/hashie/extensions/deep_merge.rb +26 -10
- data/lib/hashie/extensions/ignore_undeclared.rb +4 -5
- data/lib/hashie/extensions/indifferent_access.rb +43 -10
- data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
- data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
- data/lib/hashie/extensions/mash/keep_original_keys.rb +4 -5
- data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
- data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
- data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -6
- data/lib/hashie/extensions/method_access.rb +47 -14
- data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -4
- data/lib/hashie/extensions/ruby_version_check.rb +5 -1
- data/lib/hashie/extensions/strict_key_access.rb +16 -13
- data/lib/hashie/extensions/stringify_keys.rb +1 -1
- data/lib/hashie/extensions/symbolize_keys.rb +13 -2
- data/lib/hashie/hash.rb +18 -11
- data/lib/hashie/mash.rb +147 -81
- data/lib/hashie/railtie.rb +7 -0
- data/lib/hashie/rash.rb +6 -6
- data/lib/hashie/utils.rb +28 -0
- data/lib/hashie/version.rb +1 -1
- data/lib/hashie.rb +22 -19
- metadata +23 -131
- data/spec/hashie/array_spec.rb +0 -29
- data/spec/hashie/clash_spec.rb +0 -70
- data/spec/hashie/dash_spec.rb +0 -573
- data/spec/hashie/extensions/autoload_spec.rb +0 -24
- data/spec/hashie/extensions/coercion_spec.rb +0 -631
- data/spec/hashie/extensions/dash/coercion_spec.rb +0 -13
- data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
- data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
- data/spec/hashie/extensions/deep_find_spec.rb +0 -138
- data/spec/hashie/extensions/deep_locate_spec.rb +0 -137
- data/spec/hashie/extensions/deep_merge_spec.rb +0 -70
- data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -47
- data/spec/hashie/extensions/indifferent_access_spec.rb +0 -282
- data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
- data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
- data/spec/hashie/extensions/mash/keep_original_keys_spec.rb +0 -46
- data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -50
- data/spec/hashie/extensions/mash/symbolize_keys_spec.rb +0 -39
- data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
- data/spec/hashie/extensions/method_access_spec.rb +0 -188
- data/spec/hashie/extensions/strict_key_access_spec.rb +0 -110
- data/spec/hashie/extensions/stringify_keys_spec.rb +0 -124
- data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -129
- data/spec/hashie/hash_spec.rb +0 -84
- data/spec/hashie/mash_spec.rb +0 -763
- data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -46
- data/spec/hashie/rash_spec.rb +0 -83
- data/spec/hashie/trash_spec.rb +0 -268
- data/spec/hashie/utils_spec.rb +0 -25
- data/spec/hashie/version_spec.rb +0 -7
- data/spec/hashie_spec.rb +0 -13
- data/spec/integration/omniauth/app.rb +0 -11
- data/spec/integration/omniauth/integration_spec.rb +0 -38
- data/spec/integration/omniauth-oauth2/app.rb +0 -53
- data/spec/integration/omniauth-oauth2/integration_spec.rb +0 -26
- data/spec/integration/omniauth-oauth2/some_site.rb +0 -38
- data/spec/integration/rails/app.rb +0 -48
- data/spec/integration/rails/integration_spec.rb +0 -26
- data/spec/integration/rails-without-dependency/integration_spec.rb +0 -15
- data/spec/spec_helper.rb +0 -23
- data/spec/support/integration_specs.rb +0 -36
- data/spec/support/logger.rb +0 -24
- data/spec/support/module_context.rb +0 -11
- data/spec/support/ruby_version_check.rb +0 -6
data/UPGRADING.md
CHANGED
@@ -1,6 +1,122 @@
|
|
1
1
|
Upgrading Hashie
|
2
2
|
================
|
3
3
|
|
4
|
+
### Upgrading to 5.0.0
|
5
|
+
|
6
|
+
#### Mash initialization key conversion
|
7
|
+
|
8
|
+
Mash initialization now only converts to string keys which can be represented as symbols.
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
Hashie::Mash.new(
|
12
|
+
{foo: "bar"} => "baz",
|
13
|
+
"1" => "one string",
|
14
|
+
:"1" => "one sym",
|
15
|
+
1 => "one num"
|
16
|
+
)
|
17
|
+
|
18
|
+
# Before
|
19
|
+
{"{:foo=>\"bar\"}"=>"baz", "1"=>"one num"}
|
20
|
+
|
21
|
+
# After
|
22
|
+
{{:foo=>"bar"}=>"baz", "1"=>"one sym", 1=>"one num"}
|
23
|
+
```
|
24
|
+
|
25
|
+
#### Mash#dig with numeric keys
|
26
|
+
|
27
|
+
`Hashie::Mash#dig` no longer considers numeric keys for indifferent access.
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
my_mash = Hashie::Mash.new("1" => "a") # => {"1"=>"a"}
|
31
|
+
|
32
|
+
my_mash.dig("1") # => "a"
|
33
|
+
my_mash.dig(:"1") # => "a"
|
34
|
+
|
35
|
+
# Before
|
36
|
+
my_mash.dig(1) # => "a"
|
37
|
+
|
38
|
+
# After
|
39
|
+
my_mash.dig(1) # => nil
|
40
|
+
```
|
41
|
+
|
42
|
+
### Upgrading to 4.0.0
|
43
|
+
|
44
|
+
#### Non-destructive Hash methods called on Mash
|
45
|
+
|
46
|
+
The following non-destructive Hash methods called on Mash will now return an instance of the class it was called on.
|
47
|
+
|
48
|
+
| method | ruby |
|
49
|
+
| ----------------- | ---- |
|
50
|
+
| #compact | |
|
51
|
+
| #invert | |
|
52
|
+
| #reject | |
|
53
|
+
| #select | |
|
54
|
+
| #slice | 2.5 |
|
55
|
+
| #transform_keys | 2.5 |
|
56
|
+
| #transform_values | 2.4 |
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class Parents < Hashie::Mash; end
|
60
|
+
|
61
|
+
parents = Parents.new(father: 'Dad', mother: 'Mom')
|
62
|
+
cool_parents = parents.transform_values { |v| v + v[-1] + 'io'}
|
63
|
+
|
64
|
+
p cool_parents
|
65
|
+
|
66
|
+
# before:
|
67
|
+
{"father"=>"Daddio", "mother"=>"Mommio"}
|
68
|
+
=> {"father"=>"Daddio", "mother"=>"Mommio"}
|
69
|
+
|
70
|
+
# after:
|
71
|
+
#<Parents father="Daddio" mother="Mommio">
|
72
|
+
=> {"father"=>"Dad", "mother"=>"Mom"}
|
73
|
+
```
|
74
|
+
|
75
|
+
This may make places where you had to re-make the Mash redundant, and may cause unintended side effects if your application was expecting a plain old ruby Hash.
|
76
|
+
|
77
|
+
#### Ruby 2.6: Mash#merge and Mash#merge!
|
78
|
+
|
79
|
+
In Ruby > 2.6.0, Hashie now supports passing multiple hash and Mash objects to Mash#merge and Mash#merge!.
|
80
|
+
|
81
|
+
#### Hashie::Mash::CannotDisableMashWarnings error class is removed
|
82
|
+
|
83
|
+
There shouldn't really be a case that anyone was relying on catching this specific error, but if so, they should change it to rescue Hashie::Extensions::KeyConflictWarning::CannotDisableMashWarnings
|
84
|
+
|
85
|
+
### Upgrading to 3.7.0
|
86
|
+
|
87
|
+
#### Mash#load takes options
|
88
|
+
|
89
|
+
The `Hashie::Mash#load` method now accepts options, changing the interface of `Parser#initialize`. If you have a custom parser, you must update its `initialize` method.
|
90
|
+
|
91
|
+
For example, `Hashie::Extensions::Parsers::YamlErbParser` now accepts `permitted_classes`, `permitted_symbols` and `aliases` options.
|
92
|
+
|
93
|
+
Before:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class Hashie::Extensions::Parsers::YamlErbParser
|
97
|
+
def initialize(file_path)
|
98
|
+
@file_path = file_path
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
After:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class Hashie::Extensions::Parsers::YamlErbParser
|
107
|
+
def initialize(file_path, options = {})
|
108
|
+
@file_path = file_path
|
109
|
+
@options = options
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Options can now be passed into `Mash#load`.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
Mash.load(filename, permitted_classes: [])
|
118
|
+
```
|
119
|
+
|
4
120
|
### Upgrading to 3.5.2
|
5
121
|
|
6
122
|
#### Disable logging in Mash subclasses
|
@@ -65,7 +181,7 @@ h.abb? # => false
|
|
65
181
|
|
66
182
|
#### Possible coercion changes
|
67
183
|
|
68
|
-
The improvements made to coercions in version 3.2.1 [issue #200](https://github.com/
|
184
|
+
The improvements made to coercions in version 3.2.1 [issue #200](https://github.com/hashie/hashie/pull/200) do not break the documented API, but are significant enough that changes may effect undocumented side-effects. Applications that depended on those side-effects will need to be updated.
|
69
185
|
|
70
186
|
**Change**: Type coercion no longer creates new objects if the input matches the target type. Previously coerced properties always resulted in the creation of a new object, even when it wasn't necessary. This had the effect of a `dup` or `clone` on coerced properties but not uncoerced ones.
|
71
187
|
|
@@ -81,7 +197,7 @@ Applications that were attempting to rescuing the internal errors should be upda
|
|
81
197
|
|
82
198
|
#### Compatibility with Rails 4 Strong Parameters
|
83
199
|
|
84
|
-
Version 2.1 introduced support to prevent default Rails 4 mass-assignment protection behavior. This was [issue #89](https://github.com/
|
200
|
+
Version 2.1 introduced support to prevent default Rails 4 mass-assignment protection behavior. This was [issue #89](https://github.com/hashie/hashie/issues/89), resolved in [#104](https://github.com/hashie/hashie/pull/104). In version 2.2 this behavior has been removed in [#147](https://github.com/hashie/hashie/pull/147) in favor of a mixin and finally extracted into a separate gem in Hashie 3.0.
|
85
201
|
|
86
202
|
To enable 2.1 compatible behavior with Rails 4, use the [hashie_rails](http://rubygems.org/gems/hashie_rails) gem.
|
87
203
|
|
@@ -89,7 +205,7 @@ To enable 2.1 compatible behavior with Rails 4, use the [hashie_rails](http://ru
|
|
89
205
|
gem 'hashie_rails'
|
90
206
|
```
|
91
207
|
|
92
|
-
See [#154](https://github.com/
|
208
|
+
See [#154](https://github.com/hashie/hashie/pull/154) and [Mash and Rails 4 Strong Parameters](README.md#mash-and-rails-4-strong-parameters) for more details.
|
93
209
|
|
94
210
|
#### Key Conversions in Hashie::Dash and Hashie::Trash
|
95
211
|
|
@@ -117,7 +233,7 @@ p.inspect # => { 'name' => 'dB.' }
|
|
117
233
|
p.to_hash # => { 'name' => 'dB.' }
|
118
234
|
```
|
119
235
|
|
120
|
-
It was not possible to achieve the behavior of preserving keys, as described in [issue #151](https://github.com/
|
236
|
+
It was not possible to achieve the behavior of preserving keys, as described in [issue #151](https://github.com/hashie/hashie/issues/151).
|
121
237
|
|
122
238
|
Version 2.2 does not perform this conversion by default.
|
123
239
|
|
@@ -164,6 +280,4 @@ instance.to_hash # => { :first => 'First', "last" => 'Last' }
|
|
164
280
|
|
165
281
|
The behavior with `symbolize_keys` and `stringify_keys` is unchanged.
|
166
282
|
|
167
|
-
See [#152](https://github.com/
|
168
|
-
|
169
|
-
|
283
|
+
See [#152](https://github.com/hashie/hashie/pull/152) for more information.
|
data/hashie.gemspec
CHANGED
@@ -7,16 +7,22 @@ Gem::Specification.new do |gem|
|
|
7
7
|
gem.email = ['michael@intridea.com', 'jollyjerry@gmail.com']
|
8
8
|
gem.description = 'Hashie is a collection of classes and mixins that make hashes more powerful.'
|
9
9
|
gem.summary = 'Your friendly neighborhood hash library.'
|
10
|
-
gem.homepage = 'https://github.com/
|
10
|
+
gem.homepage = 'https://github.com/hashie/hashie'
|
11
11
|
gem.license = 'MIT'
|
12
12
|
|
13
13
|
gem.require_paths = ['lib']
|
14
|
-
gem.files
|
14
|
+
gem.files = %w[.yardopts CHANGELOG.md CONTRIBUTING.md LICENSE README.md UPGRADING.md]
|
15
|
+
gem.files += %w[Rakefile hashie.gemspec]
|
15
16
|
gem.files += Dir['lib/**/*.rb']
|
16
|
-
gem.files += Dir['spec/**/*.rb']
|
17
|
-
gem.test_files = Dir['spec/**/*.rb']
|
18
17
|
|
19
|
-
gem.
|
20
|
-
|
21
|
-
|
18
|
+
if gem.respond_to?(:metadata)
|
19
|
+
gem.metadata = {
|
20
|
+
'bug_tracker_uri' => 'https://github.com/hashie/hashie/issues',
|
21
|
+
'changelog_uri' => 'https://github.com/hashie/hashie/blob/master/CHANGELOG.md',
|
22
|
+
'documentation_uri' => 'https://www.rubydoc.info/gems/hashie',
|
23
|
+
'source_code_uri' => 'https://github.com/hashie/hashie'
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
gem.add_development_dependency 'bundler'
|
22
28
|
end
|
data/lib/hashie/clash.rb
CHANGED
@@ -75,7 +75,7 @@ module Hashie
|
|
75
75
|
when Hash
|
76
76
|
self[key] = self.class.new(self[key], self)
|
77
77
|
else
|
78
|
-
|
78
|
+
raise ChainError, 'Tried to chain into a non-hash key.'
|
79
79
|
end
|
80
80
|
elsif args.any?
|
81
81
|
merge_store(name, *args)
|
@@ -83,5 +83,16 @@ module Hashie
|
|
83
83
|
super
|
84
84
|
end
|
85
85
|
end
|
86
|
+
|
87
|
+
def respond_to_missing?(method_name, _include_private = false)
|
88
|
+
method_name = method_name.to_s
|
89
|
+
|
90
|
+
if method_name.end_with?('!')
|
91
|
+
key = method_name[0...-1].to_sym
|
92
|
+
[NilClass, Clash, Hash].include?(self[key].class)
|
93
|
+
else
|
94
|
+
true
|
95
|
+
end
|
96
|
+
end
|
86
97
|
end
|
87
98
|
end
|
data/lib/hashie/dash.rb
CHANGED
@@ -15,7 +15,7 @@ module Hashie
|
|
15
15
|
class Dash < Hash
|
16
16
|
include Hashie::Extensions::PrettyInspect
|
17
17
|
|
18
|
-
|
18
|
+
alias to_s inspect
|
19
19
|
|
20
20
|
# Defines a property on the Dash. Options are
|
21
21
|
# as follows:
|
@@ -42,30 +42,27 @@ module Hashie
|
|
42
42
|
defaults.delete property_name
|
43
43
|
end
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
property_assignment = "#{property_name}=".to_sym
|
48
|
-
define_method(property_assignment) { |value| self.[]=(property_name, value) }
|
49
|
-
end
|
45
|
+
define_getter_for(property_name)
|
46
|
+
define_setter_for(property_name)
|
50
47
|
|
51
|
-
if defined? @subclasses
|
52
|
-
@subclasses.each { |klass| klass.property(property_name, options) }
|
53
|
-
end
|
48
|
+
@subclasses.each { |klass| klass.property(property_name, options) } if defined? @subclasses
|
54
49
|
|
55
50
|
condition = options.delete(:required)
|
56
51
|
if condition
|
57
52
|
message = options.delete(:message) || "is required for #{name}."
|
58
53
|
required_properties[property_name] = { condition: condition, message: message }
|
59
|
-
|
60
|
-
|
54
|
+
elsif options.key?(:message)
|
55
|
+
raise ArgumentError, 'The :message option should be used with :required option.'
|
61
56
|
end
|
62
57
|
end
|
63
58
|
|
64
59
|
class << self
|
65
60
|
attr_reader :properties, :defaults
|
61
|
+
attr_reader :getters
|
66
62
|
attr_reader :required_properties
|
67
63
|
end
|
68
64
|
instance_variable_set('@properties', Set.new)
|
65
|
+
instance_variable_set('@getters', Set.new)
|
69
66
|
instance_variable_set('@defaults', {})
|
70
67
|
instance_variable_set('@required_properties', {})
|
71
68
|
|
@@ -73,6 +70,7 @@ module Hashie
|
|
73
70
|
super
|
74
71
|
(@subclasses ||= Set.new) << klass
|
75
72
|
klass.instance_variable_set('@properties', properties.dup)
|
73
|
+
klass.instance_variable_set('@getters', getters.dup)
|
76
74
|
klass.instance_variable_set('@defaults', defaults.dup)
|
77
75
|
klass.instance_variable_set('@required_properties', required_properties.dup)
|
78
76
|
end
|
@@ -89,30 +87,29 @@ module Hashie
|
|
89
87
|
required_properties.key? name
|
90
88
|
end
|
91
89
|
|
90
|
+
private_class_method def self.define_getter_for(property_name)
|
91
|
+
return if getters.include?(property_name)
|
92
|
+
define_method(property_name) { |&block| self.[](property_name, &block) }
|
93
|
+
getters << property_name
|
94
|
+
end
|
95
|
+
|
96
|
+
private_class_method def self.define_setter_for(property_name)
|
97
|
+
setter = :"#{property_name}="
|
98
|
+
return if instance_methods.include?(setter)
|
99
|
+
define_method(setter) { |value| self.[]=(property_name, value) }
|
100
|
+
end
|
101
|
+
|
92
102
|
# You may initialize a Dash with an attributes hash
|
93
103
|
# just like you would many other kinds of data objects.
|
94
104
|
def initialize(attributes = {}, &block)
|
95
105
|
super(&block)
|
96
106
|
|
97
|
-
self.class.defaults.each_pair do |prop, value|
|
98
|
-
self[prop] = begin
|
99
|
-
val = value.dup
|
100
|
-
if val.is_a?(Proc)
|
101
|
-
val.arity == 1 ? val.call(self) : val.call
|
102
|
-
else
|
103
|
-
val
|
104
|
-
end
|
105
|
-
rescue TypeError
|
106
|
-
value
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
107
|
initialize_attributes(attributes)
|
111
108
|
assert_required_attributes_set!
|
112
109
|
end
|
113
110
|
|
114
|
-
|
115
|
-
|
111
|
+
alias _regular_reader []
|
112
|
+
alias _regular_writer []=
|
116
113
|
private :_regular_reader, :_regular_writer
|
117
114
|
|
118
115
|
# Retrieve a value from the Dash (will return the
|
@@ -159,25 +156,48 @@ module Hashie
|
|
159
156
|
self
|
160
157
|
end
|
161
158
|
|
159
|
+
def to_h
|
160
|
+
defaults = ::Hash[self.class.properties.map { |prop| [prop, self.class.defaults[prop]] }]
|
161
|
+
|
162
|
+
defaults.merge(self)
|
163
|
+
end
|
164
|
+
alias to_hash to_h
|
165
|
+
|
162
166
|
def update_attributes!(attributes)
|
163
|
-
|
167
|
+
update_attributes(attributes)
|
164
168
|
|
165
169
|
self.class.defaults.each_pair do |prop, value|
|
170
|
+
next unless fetch(prop, nil).nil?
|
166
171
|
self[prop] = begin
|
167
|
-
value.dup
|
172
|
+
val = value.dup
|
173
|
+
if val.is_a?(Proc)
|
174
|
+
val.arity == 1 ? val.call(self) : val.call
|
175
|
+
else
|
176
|
+
val
|
177
|
+
end
|
168
178
|
rescue TypeError
|
169
179
|
value
|
170
|
-
end
|
180
|
+
end
|
171
181
|
end
|
182
|
+
|
172
183
|
assert_required_attributes_set!
|
173
184
|
end
|
174
185
|
|
175
186
|
private
|
176
187
|
|
177
188
|
def initialize_attributes(attributes)
|
189
|
+
return unless attributes
|
190
|
+
|
191
|
+
cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
|
192
|
+
update_attributes!(cleaned_attributes)
|
193
|
+
end
|
194
|
+
|
195
|
+
def update_attributes(attributes)
|
196
|
+
return unless attributes
|
197
|
+
|
178
198
|
attributes.each_pair do |att, value|
|
179
199
|
self[att] = value
|
180
|
-
end
|
200
|
+
end
|
181
201
|
end
|
182
202
|
|
183
203
|
def assert_property_exists!(property)
|
@@ -199,11 +219,12 @@ module Hashie
|
|
199
219
|
end
|
200
220
|
|
201
221
|
def fail_property_required_error!(property)
|
202
|
-
|
222
|
+
raise ArgumentError,
|
223
|
+
"The property '#{property}' #{self.class.required_properties[property][:message]}"
|
203
224
|
end
|
204
225
|
|
205
226
|
def fail_no_property_error!(property)
|
206
|
-
|
227
|
+
raise NoMethodError, "The property '#{property}' is not defined for #{self.class.name}."
|
207
228
|
end
|
208
229
|
|
209
230
|
def required?(property)
|
@@ -211,9 +232,9 @@ module Hashie
|
|
211
232
|
|
212
233
|
condition = self.class.required_properties[property][:condition]
|
213
234
|
case condition
|
214
|
-
when Proc then !!
|
215
|
-
when Symbol then !!
|
216
|
-
else !!
|
235
|
+
when Proc then !!instance_exec(&condition)
|
236
|
+
when Symbol then !!send(condition)
|
237
|
+
else !!condition
|
217
238
|
end
|
218
239
|
end
|
219
240
|
end
|
@@ -1,5 +1,9 @@
|
|
1
1
|
module Hashie
|
2
|
-
class CoercionError < StandardError
|
2
|
+
class CoercionError < StandardError
|
3
|
+
def initialize(key, value, into, message)
|
4
|
+
super("Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{message}")
|
5
|
+
end
|
6
|
+
end
|
3
7
|
|
4
8
|
module Extensions
|
5
9
|
module Coercion
|
@@ -10,22 +14,24 @@ module Hashie
|
|
10
14
|
Rational => :to_r,
|
11
15
|
String => :to_s,
|
12
16
|
Symbol => :to_sym
|
13
|
-
}
|
17
|
+
}.freeze
|
14
18
|
|
15
|
-
ABSTRACT_CORE_TYPES =
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
ABSTRACT_CORE_TYPES =
|
20
|
+
if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
|
21
|
+
{ Numeric => [Integer, Float, Complex, Rational] }
|
22
|
+
else
|
23
|
+
{
|
24
|
+
Integer => [Fixnum, Bignum],
|
25
|
+
Numeric => [Fixnum, Bignum, Float, Complex, Rational]
|
26
|
+
}
|
27
|
+
end
|
23
28
|
|
24
29
|
def self.included(base)
|
25
30
|
base.send :include, InstanceMethods
|
26
|
-
base.extend ClassMethods
|
27
|
-
|
28
|
-
|
31
|
+
base.extend ClassMethods
|
32
|
+
unless base.method_defined?(:set_value_without_coercion)
|
33
|
+
base.send :alias_method, :set_value_without_coercion, :[]=
|
34
|
+
end
|
29
35
|
base.send :alias_method, :[]=, :set_value_with_coercion
|
30
36
|
end
|
31
37
|
|
@@ -37,7 +43,7 @@ module Hashie
|
|
37
43
|
begin
|
38
44
|
value = self.class.fetch_coercion(into).call(value)
|
39
45
|
rescue NoMethodError, TypeError => e
|
40
|
-
raise CoercionError,
|
46
|
+
raise CoercionError.new(key, value, into, e.message)
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
@@ -78,7 +84,7 @@ module Hashie
|
|
78
84
|
attrs.each { |key| key_coercions[key] = into }
|
79
85
|
end
|
80
86
|
|
81
|
-
|
87
|
+
alias coerce_keys coerce_key
|
82
88
|
|
83
89
|
# Returns a hash of any existing key coercions.
|
84
90
|
def key_coercions
|
@@ -97,7 +103,8 @@ module Hashie
|
|
97
103
|
#
|
98
104
|
# @param [Class] from the type you would like coerced.
|
99
105
|
# @param [Class] into the class into which you would like the value coerced.
|
100
|
-
# @option options [Boolean] :strict (true) whether use exact source class
|
106
|
+
# @option options [Boolean] :strict (true) whether use exact source class
|
107
|
+
# only or include ancestors
|
101
108
|
#
|
102
109
|
# @example Coerce all hashes into this special type of hash
|
103
110
|
# class SpecialHash < Hash
|
@@ -159,10 +166,10 @@ module Hashie
|
|
159
166
|
|
160
167
|
def build_coercion(type)
|
161
168
|
if type.is_a? Enumerable
|
162
|
-
if type.class
|
169
|
+
if type.class == ::Hash
|
163
170
|
type, key_type, value_type = type.class, *type.first
|
164
171
|
build_hash_coercion(type, key_type, value_type)
|
165
|
-
else
|
172
|
+
else
|
166
173
|
value_type = type.first
|
167
174
|
type = type.class
|
168
175
|
build_container_coercion(type, value_type)
|
@@ -180,7 +187,7 @@ module Hashie
|
|
180
187
|
type.new(value)
|
181
188
|
end
|
182
189
|
else
|
183
|
-
|
190
|
+
raise TypeError, "#{type} is not a coercable type"
|
184
191
|
end
|
185
192
|
end
|
186
193
|
|
@@ -7,11 +7,32 @@ module Hashie
|
|
7
7
|
base.send :include, Hashie::Extensions::IndifferentAccess
|
8
8
|
end
|
9
9
|
|
10
|
+
def self.maybe_extend(base)
|
11
|
+
return unless requires_class_methods?(base)
|
12
|
+
|
13
|
+
base.extend(ClassMethods)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.requires_class_methods?(klass)
|
17
|
+
klass <= Hashie::Dash &&
|
18
|
+
!klass.singleton_class.included_modules.include?(ClassMethods)
|
19
|
+
end
|
20
|
+
private_class_method :requires_class_methods?
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
defaults = ::Hash[self.class.properties.map do |prop|
|
24
|
+
[Hashie::Extensions::IndifferentAccess.convert_key(prop), self.class.defaults[prop]]
|
25
|
+
end]
|
26
|
+
|
27
|
+
defaults.merge(self)
|
28
|
+
end
|
29
|
+
alias to_hash to_h
|
30
|
+
|
10
31
|
module ClassMethods
|
11
32
|
# Check to see if the specified property has already been
|
12
33
|
# defined.
|
13
34
|
def property?(name)
|
14
|
-
name = translations[name.to_sym] if
|
35
|
+
name = translations[name.to_sym] if translation_for?(name)
|
15
36
|
name = name.to_s
|
16
37
|
!!properties.find { |property| property.to_s == name }
|
17
38
|
end
|
@@ -30,6 +51,13 @@ module Hashie
|
|
30
51
|
name = name.to_s
|
31
52
|
!!transforms.keys.find { |key| key.to_s == name }
|
32
53
|
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def translation_for?(name)
|
58
|
+
included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
|
59
|
+
translation_exists?(name)
|
60
|
+
end
|
33
61
|
end
|
34
62
|
end
|
35
63
|
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Hashie
|
2
|
+
module Extensions
|
3
|
+
module Dash
|
4
|
+
# Extends a Dash with the ability to accept only predefined values on a property.
|
5
|
+
#
|
6
|
+
# == Example
|
7
|
+
#
|
8
|
+
# class PersonHash < Hashie::Dash
|
9
|
+
# include Hashie::Extensions::Dash::PredefinedValues
|
10
|
+
#
|
11
|
+
# property :gender, values: [:male, :female, :prefer_not_to_say]
|
12
|
+
# property :age, values: (0..150) # a Range
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# person = PersonHash.new(gender: :male, age: -1)
|
16
|
+
# # => ArgumentError: The value '-1' is not accepted for property 'age'
|
17
|
+
module PredefinedValues
|
18
|
+
def self.included(base)
|
19
|
+
base.instance_variable_set(:@values_for_properties, {})
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
base.include(InstanceMethods)
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
attr_reader :values_for_properties
|
26
|
+
|
27
|
+
def inherited(klass)
|
28
|
+
super
|
29
|
+
klass.instance_variable_set(:@values_for_properties, values_for_properties.dup)
|
30
|
+
end
|
31
|
+
|
32
|
+
def property(property_name, options = {})
|
33
|
+
super
|
34
|
+
|
35
|
+
return unless (predefined_values = options[:values])
|
36
|
+
|
37
|
+
assert_predefined_values!(predefined_values)
|
38
|
+
set_predefined_values(property_name, predefined_values)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def assert_predefined_values!(predefined_values)
|
44
|
+
return if supported_type?(predefined_values)
|
45
|
+
|
46
|
+
raise ArgumentError, %(`values` accepts an Array or a Range.)
|
47
|
+
end
|
48
|
+
|
49
|
+
def supported_type?(predefined_values)
|
50
|
+
[::Array, ::Range].any? { |klass| predefined_values.is_a?(klass) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_predefined_values(property_name, predefined_values)
|
54
|
+
@values_for_properties[property_name] = predefined_values
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
def initialize(*)
|
60
|
+
super
|
61
|
+
|
62
|
+
assert_property_values!
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def assert_property_values!
|
68
|
+
self.class.values_for_properties.each_key do |property|
|
69
|
+
value = send(property)
|
70
|
+
|
71
|
+
if value && !values_for_properties(property).include?(value)
|
72
|
+
fail_property_value_error!(property)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def fail_property_value_error!(property)
|
78
|
+
raise ArgumentError, "Invalid value for property '#{property}'"
|
79
|
+
end
|
80
|
+
|
81
|
+
def values_for_properties(property)
|
82
|
+
self.class.values_for_properties[property]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|