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.
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