hashie 3.5.7 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +281 -195
  3. data/CONTRIBUTING.md +13 -6
  4. data/LICENSE +1 -1
  5. data/README.md +320 -60
  6. data/Rakefile +2 -2
  7. data/UPGRADING.md +121 -7
  8. data/hashie.gemspec +13 -7
  9. data/lib/hashie/clash.rb +12 -1
  10. data/lib/hashie/dash.rb +56 -35
  11. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  12. data/lib/hashie/extensions/coercion.rb +26 -19
  13. data/lib/hashie/extensions/dash/indifferent_access.rb +29 -1
  14. data/lib/hashie/extensions/dash/predefined_values.rb +88 -0
  15. data/lib/hashie/extensions/dash/property_translation.rb +59 -28
  16. data/lib/hashie/extensions/deep_fetch.rb +5 -3
  17. data/lib/hashie/extensions/deep_find.rb +14 -5
  18. data/lib/hashie/extensions/deep_locate.rb +22 -8
  19. data/lib/hashie/extensions/deep_merge.rb +26 -10
  20. data/lib/hashie/extensions/ignore_undeclared.rb +4 -5
  21. data/lib/hashie/extensions/indifferent_access.rb +43 -10
  22. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  23. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  24. data/lib/hashie/extensions/mash/keep_original_keys.rb +4 -5
  25. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  26. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  27. data/lib/hashie/extensions/mash/symbolize_keys.rb +6 -6
  28. data/lib/hashie/extensions/method_access.rb +47 -14
  29. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -4
  30. data/lib/hashie/extensions/ruby_version_check.rb +5 -1
  31. data/lib/hashie/extensions/strict_key_access.rb +16 -13
  32. data/lib/hashie/extensions/stringify_keys.rb +1 -1
  33. data/lib/hashie/extensions/symbolize_keys.rb +13 -2
  34. data/lib/hashie/hash.rb +18 -11
  35. data/lib/hashie/mash.rb +147 -81
  36. data/lib/hashie/railtie.rb +7 -0
  37. data/lib/hashie/rash.rb +6 -6
  38. data/lib/hashie/utils.rb +28 -0
  39. data/lib/hashie/version.rb +1 -1
  40. data/lib/hashie.rb +22 -19
  41. metadata +23 -131
  42. data/spec/hashie/array_spec.rb +0 -29
  43. data/spec/hashie/clash_spec.rb +0 -70
  44. data/spec/hashie/dash_spec.rb +0 -573
  45. data/spec/hashie/extensions/autoload_spec.rb +0 -24
  46. data/spec/hashie/extensions/coercion_spec.rb +0 -631
  47. data/spec/hashie/extensions/dash/coercion_spec.rb +0 -13
  48. data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
  49. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
  50. data/spec/hashie/extensions/deep_find_spec.rb +0 -138
  51. data/spec/hashie/extensions/deep_locate_spec.rb +0 -137
  52. data/spec/hashie/extensions/deep_merge_spec.rb +0 -70
  53. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -47
  54. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -282
  55. data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
  56. data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
  57. data/spec/hashie/extensions/mash/keep_original_keys_spec.rb +0 -46
  58. data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -50
  59. data/spec/hashie/extensions/mash/symbolize_keys_spec.rb +0 -39
  60. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  61. data/spec/hashie/extensions/method_access_spec.rb +0 -188
  62. data/spec/hashie/extensions/strict_key_access_spec.rb +0 -110
  63. data/spec/hashie/extensions/stringify_keys_spec.rb +0 -124
  64. data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -129
  65. data/spec/hashie/hash_spec.rb +0 -84
  66. data/spec/hashie/mash_spec.rb +0 -763
  67. data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -46
  68. data/spec/hashie/rash_spec.rb +0 -83
  69. data/spec/hashie/trash_spec.rb +0 -268
  70. data/spec/hashie/utils_spec.rb +0 -25
  71. data/spec/hashie/version_spec.rb +0 -7
  72. data/spec/hashie_spec.rb +0 -13
  73. data/spec/integration/omniauth/app.rb +0 -11
  74. data/spec/integration/omniauth/integration_spec.rb +0 -38
  75. data/spec/integration/omniauth-oauth2/app.rb +0 -53
  76. data/spec/integration/omniauth-oauth2/integration_spec.rb +0 -26
  77. data/spec/integration/omniauth-oauth2/some_site.rb +0 -38
  78. data/spec/integration/rails/app.rb +0 -48
  79. data/spec/integration/rails/integration_spec.rb +0 -26
  80. data/spec/integration/rails-without-dependency/integration_spec.rb +0 -15
  81. data/spec/spec_helper.rb +0 -23
  82. data/spec/support/integration_specs.rb +0 -36
  83. data/spec/support/logger.rb +0 -24
  84. data/spec/support/module_context.rb +0 -11
  85. 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/intridea/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.
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/intridea/hashie/issues/89), resolved in [#104](https://github.com/intridea/hashie/pull/104). In version 2.2 this behavior has been removed in [#147](https://github.com/intridea/hashie/pull/147) in favor of a mixin and finally extracted into a separate gem in Hashie 3.0.
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/intridea/hashie/pull/154) and [Mash and Rails 4 Strong Parameters](README.md#mash-and-rails-4-strong-parameters) for more details.
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/intridea/hashie/issues/151).
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/intridea/hashie/pull/152) for more information.
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/intridea/hashie'
10
+ gem.homepage = 'https://github.com/hashie/hashie'
11
11
  gem.license = 'MIT'
12
12
 
13
13
  gem.require_paths = ['lib']
14
- gem.files = %w(.yardopts CHANGELOG.md CONTRIBUTING.md LICENSE README.md UPGRADING.md Rakefile hashie.gemspec)
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.add_development_dependency 'rake', '< 11'
20
- gem.add_development_dependency 'rspec', '~> 3.0'
21
- gem.add_development_dependency 'rspec-pending_for', '~> 0.1'
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
- fail ChainError, 'Tried to chain into a non-hash key.'
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
- alias_method :to_s, :inspect
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
- unless instance_methods.map(&:to_s).include?("#{property_name}=")
46
- define_method(property_name) { |&block| self.[](property_name, &block) }
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
- else
60
- fail ArgumentError, 'The :message option should be used with :required option.' if options.key?(:message)
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
- alias_method :_regular_reader, :[]
115
- alias_method :_regular_writer, :[]=
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
- initialize_attributes(attributes)
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 if self[prop].nil?
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 if attributes
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
- fail ArgumentError, "The property '#{property}' #{self.class.required_properties[property][:message]}"
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
- fail NoMethodError, "The property '#{property}' is not defined for #{self.class.name}."
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 !!(instance_exec(&condition))
215
- when Symbol then !!(send(condition))
216
- else !!(condition)
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
@@ -0,0 +1,14 @@
1
+ module Hashie
2
+ module Extensions
3
+ module ActiveSupport
4
+ module CoreExt
5
+ module Hash
6
+ def except(*keys)
7
+ string_keys = keys.map { |key| convert_key(key) }
8
+ super(*string_keys)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,9 @@
1
1
  module Hashie
2
- class CoercionError < StandardError; end
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 = if RubyVersion.new(RUBY_VERSION) >= RubyVersion.new('2.4.0')
16
- { Numeric => [Integer, Float, Complex, Rational] }
17
- else
18
- {
19
- Integer => [Fixnum, Bignum],
20
- Numeric => [Fixnum, Bignum, Float, Complex, Rational]
21
- }
22
- end
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 # NOTE: we wanna make sure we first define set_value_with_coercion before extending
27
-
28
- base.send :alias_method, :set_value_without_coercion, :[]= unless base.method_defined?(:set_value_without_coercion)
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, "Cannot coerce property #{key.inspect} from #{value.class} to #{into}: #{e.message}"
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
- alias_method :coerce_keys, :coerce_key
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 only or include ancestors
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 <= ::Hash
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 # Enumerable but not Hash: Array, Set
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
- fail TypeError, "#{type} is not a coercable type"
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 included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) && translation_exists?(name)
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