summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Herold <michael.j.herold@gmail.com>2015-04-25 22:41:14 -0500
committerMichael Herold <michael.j.herold@gmail.com>2015-04-25 23:27:44 -0500
commit717e30cae1427224dc67d83d47a531aed961bdb0 (patch)
treeeca36250c4a9092a8f68e27fd4bf471593d7697a
parentfc4b183eff6ec51270d7ec11e95536f8cf0e304a (diff)
downloadhashie-717e30cae1427224dc67d83d47a531aed961bdb0.tar.gz
Extract Trash behavior into an extension
Issue #188 brought up the fact that a Trash is really just a Dash with some extended behavior. Why don't we make that a little more explicit and define its behavior as an extension? *Follow Up* If this is merged, I move that we deprecate Trash for removal in 4.0. I think reducing the number of classes will help users better understand what behavior they are including in their subclasses. In the long run, a 4.0 release would look really good with all of the behavior factored out into extension methods. As we continue down that path, we can slowly deprecate all of the `*ash` classes with the 3.x releases. What do the other maintainers think of this suggestion? Fixes #188
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/hashie.rb1
-rw-r--r--lib/hashie/extensions/dash/property_translation.rb167
-rw-r--r--lib/hashie/trash.rb119
4 files changed, 171 insertions, 117 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index df85fe6..8fcca9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## Next Release
* [#292](https://github.com/intridea/hashie/pull/292): Removed `Mash#id` and `Mash#type` - [@jrochkind](https://github.com/jrochkind).
+* [#297](https://github.com/intridea/hashie/pull/297): Extracted `Trash`'s behavior into a new `Dash::PropertyTranslation` extension - [@michaelherold](https://github.com/michaelherold).
* Your contribution here.
## 3.4.1 (3/31/2015)
diff --git a/lib/hashie.rb b/lib/hashie.rb
index 51a9c5f..7f0433f 100644
--- a/lib/hashie.rb
+++ b/lib/hashie.rb
@@ -33,6 +33,7 @@ module Hashie
module Dash
autoload :IndifferentAccess, 'hashie/extensions/dash/indifferent_access'
+ autoload :PropertyTranslation, 'hashie/extensions/dash/property_translation'
end
module Mash
diff --git a/lib/hashie/extensions/dash/property_translation.rb b/lib/hashie/extensions/dash/property_translation.rb
new file mode 100644
index 0000000..e3d7b04
--- /dev/null
+++ b/lib/hashie/extensions/dash/property_translation.rb
@@ -0,0 +1,167 @@
+module Hashie
+ module Extensions
+ module Dash
+ # Extends a Dash with the ability to remap keys from a source hash.
+ #
+ # Property translation is useful when you need to read data from another
+ # application -- such as a Java API -- where the keys are named
+ # differently from Ruby conventions.
+ #
+ # == Example from inconsistent APIs
+ #
+ # class PersonHash < Hashie::Dash
+ # include Hashie::Extensions::Dash::PropertyTranslation
+ #
+ # property :first_name, from :firstName
+ # property :last_name, from: :lastName
+ # property :first_name, from: :f_name
+ # property :last_name, from: :l_name
+ # end
+ #
+ # person = PersonHash.new(firstName: 'Michael', l_name: 'Bleigh')
+ # person[:first_name] #=> 'Michael'
+ # person[:last_name] #=> 'Bleigh'
+ #
+ # You can also use a lambda to translate the value. This is particularly
+ # useful when you want to ensure the type of data you're wrapping.
+ #
+ # == Example using translation lambdas
+ #
+ # class DataModelHash < Hashie::Dash
+ # include Hashie::Extensions::Dash::PropertyTranslation
+ #
+ # property :id, transform_with: ->(value) { value.to_i }
+ # property :created_at, from: :created, with: ->(value) { Time.parse(value) }
+ # end
+ #
+ # model = DataModelHash.new(id: '123', created: '2014-04-25 22:35:28')
+ # model.id.class #=> Fixnum
+ # model.created_at.class #=> Time
+ module PropertyTranslation
+ def self.included(base)
+ base.instance_variable_set(:@transforms, {})
+ base.instance_variable_set(:@translations_hash, {})
+ base.extend(ClassMethods)
+ base.send(:include, InstanceMethods)
+ end
+
+ module ClassMethods
+ attr_reader :transforms, :translations_hash
+
+ # Ensures that any inheriting classes maintain their translations.
+ #
+ # * <tt>:default</tt> - The class inheriting the translations.
+ def inherited(klass)
+ super
+ klass.instance_variable_set(:@transforms, transforms.dup)
+ klass.instance_variable_set(:@translations_hash, translations_hash.dup)
+ end
+
+ def permitted_input_keys
+ @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
+ end
+
+ # Defines a property on the Trash. Options are as follows:
+ #
+ # * <tt>:default</tt> - Specify a default value for this property, to be
+ # returned before a value is set on the property in a new Dash.
+ # * <tt>:from</tt> - Specify the original key name that will be write only.
+ # * <tt>:with</tt> - Specify a lambda to be used to convert value.
+ # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
+ # without using the :from option. It transform the property itself.
+ def property(property_name, options = {})
+ super
+
+ options[:from] = options[:from] if options[:from]
+
+ if options[:from]
+ if property_name == options[:from]
+ fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
+ end
+
+ translations_hash[options[:from]] ||= {}
+ translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
+
+ define_method "#{options[:from]}=" do |val|
+ self.class.translations_hash[options[:from]].each do |name, with|
+ self[name] = with.respond_to?(:call) ? with.call(val) : val
+ end
+ end
+ else
+ if options[:transform_with].respond_to? :call
+ transforms[property_name] = options[:transform_with]
+ end
+ end
+ end
+
+ def transformed_property(property_name, value)
+ transforms[property_name].call(value)
+ end
+
+ def transformation_exists?(name)
+ transforms.key? name
+ end
+
+ def translation_exists?(name)
+ translations_hash.key? name
+ end
+
+ def translations
+ @translations ||= {}.tap do |h|
+ translations_hash.each do |(property_name, property_translations)|
+ if property_translations.size > 1
+ h[property_name] = property_translations.keys
+ else
+ h[property_name] = property_translations.keys.first
+ end
+ end
+ end
+ end
+
+ def inverse_translations
+ @inverse_translations ||= {}.tap do |h|
+ translations_hash.each do |(property_name, property_translations)|
+ property_translations.keys.each do |k|
+ h[k] = property_name
+ end
+ end
+ end
+ end
+ end
+
+ module InstanceMethods
+ # Sets a value on the Dash in a Hash-like way.
+ #
+ # Note: Only works on pre-existing properties.
+ def []=(property, value)
+ if self.class.translation_exists? property
+ send("#{property}=", value)
+ elsif self.class.transformation_exists? property
+ super property, self.class.transformed_property(property, value)
+ elsif property_exists? property
+ super
+ end
+ end
+
+ # Deletes any keys that have a translation
+ def initialize_attributes(attributes)
+ return unless attributes
+ attributes_copy = attributes.dup.delete_if do |k, v|
+ if self.class.translations_hash.include?(k)
+ self[k] = v
+ true
+ end
+ end
+ super attributes_copy
+ end
+
+ # Raises an NoMethodError if the property doesn't exist
+ def property_exists?(property)
+ fail_no_property_error!(property) unless self.class.property?(property)
+ true
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/hashie/trash.rb b/lib/hashie/trash.rb
index dbf4dae..2474fd8 100644
--- a/lib/hashie/trash.rb
+++ b/lib/hashie/trash.rb
@@ -1,4 +1,5 @@
require 'hashie/dash'
+require 'hashie/extensions/dash/property_translation'
module Hashie
# A Trash is a 'translated' Dash where the keys can be remapped from a source
@@ -8,122 +9,6 @@ module Hashie
# such as a Java api, where the keys are named differently from how we would
# in Ruby.
class Trash < Dash
- # Defines a property on the Trash. Options are as follows:
- #
- # * <tt>:default</tt> - Specify a default value for this property, to be
- # returned before a value is set on the property in a new Dash.
- # * <tt>:from</tt> - Specify the original key name that will be write only.
- # * <tt>:with</tt> - Specify a lambda to be used to convert value.
- # * <tt>:transform_with</tt> - Specify a lambda to be used to convert value
- # without using the :from option. It transform the property itself.
- def self.property(property_name, options = {})
- super
-
- options[:from] = options[:from] if options[:from]
-
- if options[:from]
- if property_name == options[:from]
- fail ArgumentError, "Property name (#{property_name}) and :from option must not be the same"
- end
-
- translations_hash[options[:from]] ||= {}
- translations_hash[options[:from]][property_name] = options[:with] || options[:transform_with]
-
- define_method "#{options[:from]}=" do |val|
- self.class.translations_hash[options[:from]].each do |name, with|
- self[name] = with.respond_to?(:call) ? with.call(val) : val
- end
- end
- else
- if options[:transform_with].respond_to? :call
- transforms[property_name] = options[:transform_with]
- end
- end
- end
-
- class << self
- attr_reader :transforms, :translations_hash
- end
- instance_variable_set('@transforms', {})
- instance_variable_set('@translations_hash', {})
-
- def self.inherited(klass)
- super
- klass.instance_variable_set('@transforms', transforms.dup)
- klass.instance_variable_set('@translations_hash', translations_hash.dup)
- end
-
- # Set a value on the Dash in a Hash-like way. Only works
- # on pre-existing properties.
- def []=(property, value)
- if self.class.translation_exists? property
- send("#{property}=", value)
- elsif self.class.transformation_exists? property
- super property, self.class.transformed_property(property, value)
- elsif property_exists? property
- super
- end
- end
-
- def self.transformed_property(property_name, value)
- transforms[property_name].call(value)
- end
-
- def self.translation_exists?(name)
- translations_hash.key? name
- end
-
- def self.transformation_exists?(name)
- transforms.key? name
- end
-
- def self.permitted_input_keys
- @permitted_input_keys ||= properties.map { |property| inverse_translations.fetch property, property }
- end
-
- private
-
- def self.translations
- @translations ||= begin
- h = {}
- translations_hash.each do |(property_name, property_translations)|
- h[property_name] = property_translations.size > 1 ? property_translations.keys : property_translations.keys.first
- end
- h
- end
- end
-
- def self.inverse_translations
- @inverse_translations ||= begin
- h = {}
- translations_hash.each do |(property_name, property_translations)|
- property_translations.keys.each do |k|
- h[k] = property_name
- end
- end
- h
- end
- end
-
- # Raises an NoMethodError if the property doesn't exist
- #
- def property_exists?(property)
- fail_no_property_error!(property) unless self.class.property?(property)
- true
- end
-
- private
-
- # Deletes any keys that have a translation
- def initialize_attributes(attributes)
- return unless attributes
- attributes_copy = attributes.dup.delete_if do |k, v|
- if self.class.translations_hash.include?(k)
- self[k] = v
- true
- end
- end
- super attributes_copy
- end
+ include Hashie::Extensions::Dash::PropertyTranslation
end
end