hashie 3.5.7 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|