diff options
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | lib/hashie.rb | 1 | ||||
-rw-r--r-- | lib/hashie/extensions/dash/property_translation.rb | 167 | ||||
-rw-r--r-- | lib/hashie/trash.rb | 119 |
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 |