hashie 3.5.7 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +240 -164
  3. data/CONTRIBUTING.md +13 -6
  4. data/README.md +210 -29
  5. data/Rakefile +2 -2
  6. data/UPGRADING.md +83 -7
  7. data/hashie.gemspec +13 -7
  8. data/lib/hashie.rb +21 -19
  9. data/lib/hashie/clash.rb +12 -1
  10. data/lib/hashie/dash.rb +42 -21
  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 +20 -1
  14. data/lib/hashie/extensions/dash/property_translation.rb +54 -28
  15. data/lib/hashie/extensions/deep_fetch.rb +5 -3
  16. data/lib/hashie/extensions/deep_find.rb +14 -5
  17. data/lib/hashie/extensions/deep_locate.rb +22 -8
  18. data/lib/hashie/extensions/deep_merge.rb +26 -10
  19. data/lib/hashie/extensions/indifferent_access.rb +8 -8
  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 +4 -5
  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 +1 -1
  26. data/lib/hashie/extensions/method_access.rb +47 -14
  27. data/lib/hashie/extensions/parsers/yaml_erb_parser.rb +28 -4
  28. data/lib/hashie/extensions/ruby_version_check.rb +5 -1
  29. data/lib/hashie/extensions/strict_key_access.rb +16 -13
  30. data/lib/hashie/extensions/stringify_keys.rb +1 -1
  31. data/lib/hashie/extensions/symbolize_keys.rb +1 -1
  32. data/lib/hashie/hash.rb +18 -11
  33. data/lib/hashie/mash.rb +131 -70
  34. data/lib/hashie/railtie.rb +7 -0
  35. data/lib/hashie/rash.rb +6 -6
  36. data/lib/hashie/utils.rb +28 -0
  37. data/lib/hashie/version.rb +1 -1
  38. metadata +19 -128
  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 -573
  42. data/spec/hashie/extensions/autoload_spec.rb +0 -24
  43. data/spec/hashie/extensions/coercion_spec.rb +0 -631
  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 -282
  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 -188
  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 -763
  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 -268
  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/omniauth-oauth2/app.rb +0 -53
  71. data/spec/integration/omniauth-oauth2/integration_spec.rb +0 -26
  72. data/spec/integration/omniauth-oauth2/some_site.rb +0 -38
  73. data/spec/integration/omniauth/app.rb +0 -11
  74. data/spec/integration/omniauth/integration_spec.rb +0 -38
  75. data/spec/integration/rails-without-dependency/integration_spec.rb +0 -15
  76. data/spec/integration/rails/app.rb +0 -48
  77. data/spec/integration/rails/integration_spec.rb +0 -26
  78. data/spec/spec_helper.rb +0 -23
  79. data/spec/support/integration_specs.rb +0 -36
  80. data/spec/support/logger.rb +0 -24
  81. data/spec/support/module_context.rb +0 -11
  82. data/spec/support/ruby_version_check.rb +0 -6
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.rb CHANGED
@@ -12,26 +12,26 @@ module Hashie
12
12
  autoload :Utils, 'hashie/utils'
13
13
 
14
14
  module Extensions
15
- autoload :Coercion, 'hashie/extensions/coercion'
16
- autoload :DeepMerge, 'hashie/extensions/deep_merge'
17
- autoload :IgnoreUndeclared, 'hashie/extensions/ignore_undeclared'
18
- autoload :IndifferentAccess, 'hashie/extensions/indifferent_access'
19
- autoload :MergeInitializer, 'hashie/extensions/merge_initializer'
20
- autoload :MethodAccess, 'hashie/extensions/method_access'
21
- autoload :MethodQuery, 'hashie/extensions/method_access'
22
- autoload :MethodReader, 'hashie/extensions/method_access'
23
- autoload :MethodWriter, 'hashie/extensions/method_access'
24
- autoload :StringifyKeys, 'hashie/extensions/stringify_keys'
25
- autoload :SymbolizeKeys, 'hashie/extensions/symbolize_keys'
26
- autoload :DeepFetch, 'hashie/extensions/deep_fetch'
27
- autoload :DeepFind, 'hashie/extensions/deep_find'
28
- autoload :DeepLocate, 'hashie/extensions/deep_locate'
29
- autoload :PrettyInspect, 'hashie/extensions/pretty_inspect'
30
- autoload :KeyConversion, 'hashie/extensions/key_conversion'
15
+ autoload :Coercion, 'hashie/extensions/coercion'
16
+ autoload :DeepMerge, 'hashie/extensions/deep_merge'
17
+ autoload :IgnoreUndeclared, 'hashie/extensions/ignore_undeclared'
18
+ autoload :IndifferentAccess, 'hashie/extensions/indifferent_access'
19
+ autoload :MergeInitializer, 'hashie/extensions/merge_initializer'
20
+ autoload :MethodAccess, 'hashie/extensions/method_access'
21
+ autoload :MethodQuery, 'hashie/extensions/method_access'
22
+ autoload :MethodReader, 'hashie/extensions/method_access'
23
+ autoload :MethodWriter, 'hashie/extensions/method_access'
24
+ autoload :StringifyKeys, 'hashie/extensions/stringify_keys'
25
+ autoload :SymbolizeKeys, 'hashie/extensions/symbolize_keys'
26
+ autoload :DeepFetch, 'hashie/extensions/deep_fetch'
27
+ autoload :DeepFind, 'hashie/extensions/deep_find'
28
+ autoload :DeepLocate, 'hashie/extensions/deep_locate'
29
+ autoload :PrettyInspect, 'hashie/extensions/pretty_inspect'
30
+ autoload :KeyConversion, 'hashie/extensions/key_conversion'
31
31
  autoload :MethodAccessWithOverride, 'hashie/extensions/method_access'
32
- autoload :StrictKeyAccess, 'hashie/extensions/strict_key_access'
33
- autoload :RubyVersion, 'hashie/extensions/ruby_version'
34
- autoload :RubyVersionCheck, 'hashie/extensions/ruby_version_check'
32
+ autoload :StrictKeyAccess, 'hashie/extensions/strict_key_access'
33
+ autoload :RubyVersion, 'hashie/extensions/ruby_version'
34
+ autoload :RubyVersionCheck, 'hashie/extensions/ruby_version_check'
35
35
 
36
36
  module Parsers
37
37
  autoload :YamlErbParser, 'hashie/extensions/parsers/yaml_erb_parser'
@@ -45,8 +45,10 @@ module Hashie
45
45
 
46
46
  module Mash
47
47
  autoload :KeepOriginalKeys, 'hashie/extensions/mash/keep_original_keys'
48
+ autoload :PermissiveRespondTo, 'hashie/extensions/mash/permissive_respond_to'
48
49
  autoload :SafeAssignment, 'hashie/extensions/mash/safe_assignment'
49
50
  autoload :SymbolizeKeys, 'hashie/extensions/mash/symbolize_keys'
51
+ autoload :DefineAccessors, 'hashie/extensions/mash/define_accessors'
50
52
  end
51
53
 
52
54
  module Array
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,6 +87,18 @@ 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)
@@ -111,8 +121,8 @@ module Hashie
111
121
  assert_required_attributes_set!
112
122
  end
113
123
 
114
- alias_method :_regular_reader, :[]
115
- alias_method :_regular_writer, :[]=
124
+ alias _regular_reader []
125
+ alias _regular_writer []=
116
126
  private :_regular_reader, :_regular_writer
117
127
 
118
128
  # Retrieve a value from the Dash (will return the
@@ -160,14 +170,15 @@ module Hashie
160
170
  end
161
171
 
162
172
  def update_attributes!(attributes)
163
- initialize_attributes(attributes)
173
+ update_attributes(attributes)
164
174
 
165
175
  self.class.defaults.each_pair do |prop, value|
176
+ next unless self[prop].nil?
166
177
  self[prop] = begin
167
178
  value.dup
168
179
  rescue TypeError
169
180
  value
170
- end if self[prop].nil?
181
+ end
171
182
  end
172
183
  assert_required_attributes_set!
173
184
  end
@@ -175,9 +186,18 @@ module Hashie
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,23 @@ 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
+
10
22
  module ClassMethods
11
23
  # Check to see if the specified property has already been
12
24
  # defined.
13
25
  def property?(name)
14
- name = translations[name.to_sym] if included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) && translation_exists?(name)
26
+ name = translations[name.to_sym] if translation_for?(name)
15
27
  name = name.to_s
16
28
  !!properties.find { |property| property.to_s == name }
17
29
  end
@@ -30,6 +42,13 @@ module Hashie
30
42
  name = name.to_s
31
43
  !!transforms.keys.find { |key| key.to_s == name }
32
44
  end
45
+
46
+ private
47
+
48
+ def translation_for?(name)
49
+ included_modules.include?(Hashie::Extensions::Dash::PropertyTranslation) &&
50
+ translation_exists?(name)
51
+ end
33
52
  end
34
53
  end
35
54
  end
@@ -35,12 +35,12 @@ 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)
42
42
  base.instance_variable_set(:@transforms, {})
43
- base.instance_variable_set(:@translations_hash, {})
43
+ base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
44
44
  base.extend(ClassMethods)
45
45
  base.send(:include, InstanceMethods)
46
46
  end
@@ -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:
@@ -72,23 +74,16 @@ module Hashie
72
74
  def property(property_name, options = {})
73
75
  super
74
76
 
75
- if options[:from]
76
- if property_name == options[:from]
77
- fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
78
- end
79
-
80
- translations_hash[options[:from]] ||= {}
81
- translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
77
+ from = options[:from]
78
+ converter = options[:with]
79
+ transformer = options[:transform_with]
82
80
 
83
- define_method "#{options[:from]}=" do |val|
84
- self.class.translations_hash[options[:from]].each do |name, with|
85
- self[name] = with.respond_to?(:call) ? with.call(val) : val
86
- end
87
- end
88
- else
89
- if options[:transform_with].respond_to? :call
90
- transforms[property_name] = options[:transform_with]
91
- end
81
+ if from
82
+ fail_self_transformation_error!(property_name) if property_name == from
83
+ define_translation(from, property_name, converter || transformer)
84
+ define_writer_for_source_property(from)
85
+ elsif valid_transformer?(transformer)
86
+ transforms[property_name] = transformer
92
87
  end
93
88
  end
94
89
 
@@ -105,26 +100,50 @@ module Hashie
105
100
  end
106
101
 
107
102
  def translations
108
- @translations ||= {}.tap do |h|
103
+ @translations ||= {}.tap do |translations|
109
104
  translations_hash.each do |(property_name, property_translations)|
110
- if property_translations.size > 1
111
- h[property_name] = property_translations.keys
112
- else
113
- h[property_name] = property_translations.keys.first
114
- end
105
+ translations[property_name] =
106
+ if property_translations.size > 1
107
+ property_translations.keys
108
+ else
109
+ property_translations.keys.first
110
+ end
115
111
  end
116
112
  end
117
113
  end
118
114
 
119
115
  def inverse_translations
120
- @inverse_translations ||= {}.tap do |h|
116
+ @inverse_translations ||= {}.tap do |translations|
121
117
  translations_hash.each do |(property_name, property_translations)|
122
- property_translations.keys.each do |k|
123
- h[k] = property_name
118
+ property_translations.each_key do |key|
119
+ translations[key] = property_name
124
120
  end
125
121
  end
126
122
  end
127
123
  end
124
+
125
+ private
126
+
127
+ def define_translation(from, property_name, translator)
128
+ translations_hash[from][property_name] = translator
129
+ end
130
+
131
+ def define_writer_for_source_property(property)
132
+ define_method "#{property}=" do |val|
133
+ __translations[property].each do |name, with|
134
+ self[name] = with.respond_to?(:call) ? with.call(val) : val
135
+ end
136
+ end
137
+ end
138
+
139
+ def fail_self_transformation_error!(property_name)
140
+ raise ArgumentError,
141
+ "Property name (#{property_name}) and :from option must not be the same"
142
+ end
143
+
144
+ def valid_transformer?(transformer)
145
+ transformer.respond_to? :call
146
+ end
128
147
  end
129
148
 
130
149
  module InstanceMethods
@@ -134,6 +153,7 @@ module Hashie
134
153
  def []=(property, value)
135
154
  if self.class.translation_exists? property
136
155
  send("#{property}=", value)
156
+ super(property, value) if self.class.properties.include?(property)
137
157
  elsif self.class.transformation_exists? property
138
158
  super property, self.class.transformed_property(property, value)
139
159
  elsif property_exists? property
@@ -158,6 +178,12 @@ module Hashie
158
178
  fail_no_property_error!(property) unless self.class.property?(property)
159
179
  true
160
180
  end
181
+
182
+ private
183
+
184
+ def __translations
185
+ self.class.translations_hash
186
+ end
161
187
  end
162
188
  end
163
189
  end