hashie 3.6.0 → 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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +271 -207
  3. data/CONTRIBUTING.md +13 -6
  4. data/LICENSE +1 -1
  5. data/README.md +245 -54
  6. data/UPGRADING.md +121 -7
  7. data/hashie.gemspec +13 -7
  8. data/lib/hashie/dash.rb +18 -17
  9. data/lib/hashie/extensions/active_support/core_ext/hash.rb +14 -0
  10. data/lib/hashie/extensions/coercion.rb +23 -16
  11. data/lib/hashie/extensions/dash/indifferent_access.rb +29 -1
  12. data/lib/hashie/extensions/dash/predefined_values.rb +88 -0
  13. data/lib/hashie/extensions/dash/property_translation.rb +12 -4
  14. data/lib/hashie/extensions/deep_fetch.rb +4 -2
  15. data/lib/hashie/extensions/deep_find.rb +12 -3
  16. data/lib/hashie/extensions/deep_locate.rb +22 -7
  17. data/lib/hashie/extensions/deep_merge.rb +18 -1
  18. data/lib/hashie/extensions/ignore_undeclared.rb +4 -5
  19. data/lib/hashie/extensions/indifferent_access.rb +37 -6
  20. data/lib/hashie/extensions/key_conflict_warning.rb +55 -0
  21. data/lib/hashie/extensions/mash/define_accessors.rb +90 -0
  22. data/lib/hashie/extensions/mash/keep_original_keys.rb +2 -1
  23. data/lib/hashie/extensions/mash/permissive_respond_to.rb +61 -0
  24. data/lib/hashie/extensions/mash/safe_assignment.rb +3 -1
  25. data/lib/hashie/extensions/mash/symbolize_keys.rb +5 -5
  26. data/lib/hashie/extensions/method_access.rb +5 -2
  27. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +26 -4
  28. data/lib/hashie/extensions/ruby_version_check.rb +5 -1
  29. data/lib/hashie/extensions/strict_key_access.rb +8 -4
  30. data/lib/hashie/extensions/symbolize_keys.rb +12 -1
  31. data/lib/hashie/hash.rb +16 -9
  32. data/lib/hashie/mash.rb +130 -64
  33. data/lib/hashie/railtie.rb +7 -0
  34. data/lib/hashie/rash.rb +1 -1
  35. data/lib/hashie/utils.rb +28 -0
  36. data/lib/hashie/version.rb +1 -1
  37. data/lib/hashie.rb +22 -19
  38. metadata +23 -133
  39. data/spec/hashie/array_spec.rb +0 -29
  40. data/spec/hashie/clash_spec.rb +0 -70
  41. data/spec/hashie/dash_spec.rb +0 -598
  42. data/spec/hashie/extensions/autoload_spec.rb +0 -24
  43. data/spec/hashie/extensions/coercion_spec.rb +0 -639
  44. data/spec/hashie/extensions/dash/coercion_spec.rb +0 -13
  45. data/spec/hashie/extensions/dash/indifferent_access_spec.rb +0 -84
  46. data/spec/hashie/extensions/deep_fetch_spec.rb +0 -97
  47. data/spec/hashie/extensions/deep_find_spec.rb +0 -138
  48. data/spec/hashie/extensions/deep_locate_spec.rb +0 -137
  49. data/spec/hashie/extensions/deep_merge_spec.rb +0 -70
  50. data/spec/hashie/extensions/ignore_undeclared_spec.rb +0 -47
  51. data/spec/hashie/extensions/indifferent_access_spec.rb +0 -295
  52. data/spec/hashie/extensions/indifferent_access_with_rails_hwia_spec.rb +0 -208
  53. data/spec/hashie/extensions/key_conversion_spec.rb +0 -12
  54. data/spec/hashie/extensions/mash/keep_original_keys_spec.rb +0 -46
  55. data/spec/hashie/extensions/mash/safe_assignment_spec.rb +0 -50
  56. data/spec/hashie/extensions/mash/symbolize_keys_spec.rb +0 -39
  57. data/spec/hashie/extensions/merge_initializer_spec.rb +0 -23
  58. data/spec/hashie/extensions/method_access_spec.rb +0 -226
  59. data/spec/hashie/extensions/strict_key_access_spec.rb +0 -110
  60. data/spec/hashie/extensions/stringify_keys_spec.rb +0 -124
  61. data/spec/hashie/extensions/symbolize_keys_spec.rb +0 -129
  62. data/spec/hashie/hash_spec.rb +0 -84
  63. data/spec/hashie/mash_spec.rb +0 -771
  64. data/spec/hashie/parsers/yaml_erb_parser_spec.rb +0 -46
  65. data/spec/hashie/rash_spec.rb +0 -83
  66. data/spec/hashie/trash_spec.rb +0 -328
  67. data/spec/hashie/utils_spec.rb +0 -25
  68. data/spec/hashie/version_spec.rb +0 -7
  69. data/spec/hashie_spec.rb +0 -13
  70. data/spec/integration/elasticsearch/integration_spec.rb +0 -40
  71. data/spec/integration/omniauth/app.rb +0 -11
  72. data/spec/integration/omniauth/integration_spec.rb +0 -38
  73. data/spec/integration/omniauth-oauth2/app.rb +0 -52
  74. data/spec/integration/omniauth-oauth2/integration_spec.rb +0 -26
  75. data/spec/integration/omniauth-oauth2/some_site.rb +0 -38
  76. data/spec/integration/rails/app.rb +0 -47
  77. data/spec/integration/rails/integration_spec.rb +0 -26
  78. data/spec/integration/rails-without-dependency/integration_spec.rb +0 -15
  79. data/spec/spec_helper.rb +0 -23
  80. data/spec/support/integration_specs.rb +0 -36
  81. data/spec/support/logger.rb +0 -24
  82. data/spec/support/module_context.rb +0 -11
  83. 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/dash.rb CHANGED
@@ -104,19 +104,6 @@ module Hashie
104
104
  def initialize(attributes = {}, &block)
105
105
  super(&block)
106
106
 
107
- self.class.defaults.each_pair do |prop, value|
108
- self[prop] = begin
109
- val = value.dup
110
- if val.is_a?(Proc)
111
- val.arity == 1 ? val.call(self) : val.call
112
- else
113
- val
114
- end
115
- rescue TypeError
116
- value
117
- end
118
- end
119
-
120
107
  initialize_attributes(attributes)
121
108
  assert_required_attributes_set!
122
109
  end
@@ -169,17 +156,30 @@ module Hashie
169
156
  self
170
157
  end
171
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
+
172
166
  def update_attributes!(attributes)
173
167
  update_attributes(attributes)
174
168
 
175
169
  self.class.defaults.each_pair do |prop, value|
176
- next unless self[prop].nil?
170
+ next unless fetch(prop, nil).nil?
177
171
  self[prop] = begin
178
- 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
179
178
  rescue TypeError
180
179
  value
181
180
  end
182
181
  end
182
+
183
183
  assert_required_attributes_set!
184
184
  end
185
185
 
@@ -189,7 +189,7 @@ module Hashie
189
189
  return unless attributes
190
190
 
191
191
  cleaned_attributes = attributes.reject { |_attr, value| value.nil? }
192
- update_attributes(cleaned_attributes)
192
+ update_attributes!(cleaned_attributes)
193
193
  end
194
194
 
195
195
  def update_attributes(attributes)
@@ -219,7 +219,8 @@ module Hashie
219
219
  end
220
220
 
221
221
  def fail_property_required_error!(property)
222
- raise ArgumentError, "The property '#{property}' #{self.class.required_properties[property][:message]}"
222
+ raise ArgumentError,
223
+ "The property '#{property}' #{self.class.required_properties[property][:message]}"
223
224
  end
224
225
 
225
226
  def fail_no_property_error!(property)
@@ -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
@@ -12,20 +16,22 @@ module Hashie
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], # rubocop:disable Lint/UnifiedInteger
20
- Numeric => [Fixnum, Bignum, Float, Complex, Rational] # rubocop:disable Lint/UnifiedInteger
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
 
@@ -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)
@@ -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
@@ -35,7 +35,7 @@ module Hashie
35
35
  # end
36
36
  #
37
37
  # model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
38
- # model.id.class #=> Fixnum
38
+ # model.id.class #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
39
39
  # model.created_at.class #=> Time
40
40
  module PropertyTranslation
41
41
  def self.included(base)
@@ -58,7 +58,9 @@ module Hashie
58
58
  end
59
59
 
60
60
  def permitted_input_keys
61
- @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
61
+ @permitted_input_keys ||=
62
+ properties
63
+ .map { |property| inverse_translations.fetch property, property }
62
64
  end
63
65
 
64
66
  # Defines a property on the Trash. Options are as follows:
@@ -135,7 +137,8 @@ module Hashie
135
137
  end
136
138
 
137
139
  def fail_self_transformation_error!(property_name)
138
- raise ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
140
+ raise ArgumentError,
141
+ "Property name (#{property_name}) and :from option must not be the same"
139
142
  end
140
143
 
141
144
  def valid_transformer?(transformer)
@@ -150,7 +153,12 @@ module Hashie
150
153
  def []=(property, value)
151
154
  if self.class.translation_exists? property
152
155
  send("#{property}=", value)
153
- super(property, value) if self.class.properties.include?(property)
156
+
157
+ if self.class.transformation_exists? property
158
+ super property, self.class.transformed_property(property, value)
159
+ elsif self.class.properties.include?(property)
160
+ super(property, value)
161
+ end
154
162
  elsif self.class.transformation_exists? property
155
163
  super property, self.class.transformed_property(property, value)
156
164
  elsif property_exists? property
@@ -9,7 +9,8 @@ module Hashie
9
9
  #
10
10
  # options.deep_fetch(:user, :non_existent_key) { 'a value' } #=> 'a value'
11
11
  #
12
- # This is particularly useful for fetching values from deeply nested api responses or params hashes.
12
+ # This is particularly useful for fetching values from deeply nested api responses
13
+ # or params hashes.
13
14
  module DeepFetch
14
15
  class UndefinedPathError < StandardError; end
15
16
 
@@ -20,7 +21,8 @@ module Hashie
20
21
  obj.fetch(arg)
21
22
  rescue ArgumentError, IndexError, NoMethodError => e
22
23
  break yield(arg) if block
23
- raise UndefinedPathError, "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
+ raise UndefinedPathError,
25
+ "Could not fetch path (#{args.join(' > ')}) at #{arg}", e.backtrace
24
26
  end
25
27
  end
26
28
  end
@@ -1,3 +1,4 @@
1
+ require 'hashie/extensions/deep_locate'
1
2
  module Hashie
2
3
  module Extensions
3
4
  module DeepFind
@@ -24,7 +25,12 @@ module Hashie
24
25
  # Performs a depth-first search on deeply nested data structures for
25
26
  # a key and returns all occurrences of the key.
26
27
  #
27
- # options = {users: [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]}
28
+ # options = {
29
+ # users: [
30
+ # { location: {address: '123 Street'} },
31
+ # { location: {address: '234 Street'}}
32
+ # ]
33
+ # }
28
34
  # options.extend(Hashie::Extensions::DeepFind)
29
35
  # options.deep_find_all(:address) # => ['123 Street', '234 Street']
30
36
  #
@@ -33,7 +39,10 @@ module Hashie
33
39
  # end
34
40
  #
35
41
  # my_hash = MyHash.new
36
- # my_hash[:users] = [{location: {address: '123 Street'}}, {location: {address: '234 Street'}}]
42
+ # my_hash[:users] = [
43
+ # {location: {address: '123 Street'}},
44
+ # {location: {address: '234 Street'}}
45
+ # ]
37
46
  # my_hash.deep_find_all(:address) # => ['123 Street', '234 Street']
38
47
  def deep_find_all(key)
39
48
  matches = _deep_find_all(key)
@@ -49,7 +58,7 @@ module Hashie
49
58
  end
50
59
 
51
60
  def _deep_find_all(key, object = self, matches = [])
52
- deep_locate_result = Hashie::Extensions::DeepLocate.deep_locate(key, object).tap do |result|
61
+ deep_locate_result = DeepLocate.deep_locate(key, object).tap do |result|
53
62
  result.map! { |element| element[key] }
54
63
  end
55
64
 
@@ -14,10 +14,12 @@ module Hashie
14
14
  # ...
15
15
  # ]
16
16
  #
17
- # Hashie::Extensions::DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
17
+ # DeepLocate.deep_locate -> (key, value, object) { key == :title }, books
18
18
  # # => [{:title=>"Ruby for beginners", :pages=>120}, ...]
19
19
  def self.deep_locate(comparator, object)
20
- comparator = _construct_key_comparator(comparator, object) unless comparator.respond_to?(:call)
20
+ unless comparator.respond_to?(:call)
21
+ comparator = _construct_key_comparator(comparator, object)
22
+ end
21
23
 
22
24
  _deep_locate(comparator, object)
23
25
  end
@@ -53,17 +55,21 @@ module Hashie
53
55
  # # http://ruby-journal.com/becareful-with-space-in-lambda-hash-rocket-syntax-between-ruby-1-dot-9-and-2-dot-0/
54
56
  #
55
57
  # books.deep_locate -> (key, value, object) { key == :title && value.include?("Ruby") }
56
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"Ruby for the rest of us", :pages=>576}]
58
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
59
+ # # {:title=>"Ruby for the rest of us", :pages=>576}]
57
60
  #
58
61
  # books.deep_locate -> (key, value, object) { key == :pages && value <= 120 }
59
- # # => [{:title=>"Ruby for beginners", :pages=>120}, {:title=>"CSS for intermediates", :pages=>80}]
62
+ # # => [{:title=>"Ruby for beginners", :pages=>120},
63
+ # # {:title=>"CSS for intermediates", :pages=>80}]
60
64
  def deep_locate(comparator)
61
65
  Hashie::Extensions::DeepLocate.deep_locate(comparator, self)
62
66
  end
63
67
 
64
68
  def self._construct_key_comparator(search_key, object)
65
- search_key = search_key.to_s if defined?(::ActiveSupport::HashWithIndifferentAccess) && object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
66
- search_key = search_key.to_s if object.respond_to?(:indifferent_access?) && object.indifferent_access?
69
+ if object.respond_to?(:indifferent_access?) && object.indifferent_access? ||
70
+ activesupport_indifferent?(object)
71
+ search_key = search_key.to_s
72
+ end
67
73
 
68
74
  lambda do |non_callable_object|
69
75
  ->(key, _, _) { key == non_callable_object }
@@ -73,7 +79,10 @@ module Hashie
73
79
 
74
80
  def self._deep_locate(comparator, object, result = [])
75
81
  if object.is_a?(::Enumerable)
76
- result.push object if object.any? { |value| _match_comparator?(value, comparator, object) }
82
+ if object.any? { |value| _match_comparator?(value, comparator, object) }
83
+ result.push object
84
+ end
85
+
77
86
  (object.respond_to?(:values) ? object.values : object.entries).each do |value|
78
87
  _deep_locate(comparator, value, result)
79
88
  end
@@ -93,6 +102,12 @@ module Hashie
93
102
  comparator.call(key, value, object)
94
103
  end
95
104
  private_class_method :_match_comparator?
105
+
106
+ def self.activesupport_indifferent?(object)
107
+ defined?(::ActiveSupport::HashWithIndifferentAccess) &&
108
+ object.is_a?(::ActiveSupport::HashWithIndifferentAccess)
109
+ end
110
+ private_class_method :activesupport_indifferent?
96
111
  end
97
112
  end
98
113
  end