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 #=> Integer (Fixnum if you are using Ruby 2.3 or lower)
# model.created_at.class #=> Time
module PropertyTranslation
def self.included(base)
base.instance_variable_set(:@transforms, {})
base.instance_variable_set(:@translations_hash, ::Hash.new { |hash, key| hash[key] = {} })
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
end
module ClassMethods
attr_reader :transforms, :translations_hash
# Ensures that any inheriting classes maintain their translations.
#
# * :default - 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:
#
# * :default - Specify a default value for this property, to be
# returned before a value is set on the property in a new Dash.
# * :from - Specify the original key name that will be write only.
# * :with - Specify a lambda to be used to convert value.
# * :transform_with - 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
from = options[:from]
converter = options[:with]
transformer = options[:transform_with]
if from
fail_self_transformation_error!(property_name) if property_name == from
define_translation(from, property_name, converter || transformer)
define_writer_for_source_property(from)
elsif valid_transformer?(transformer)
transforms[property_name] = transformer
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 |translations|
translations_hash.each do |(property_name, property_translations)|
translations[property_name] =
if property_translations.size > 1
property_translations.keys
else
property_translations.keys.first
end
end
end
end
def inverse_translations
@inverse_translations ||= {}.tap do |translations|
translations_hash.each do |(property_name, property_translations)|
property_translations.each_key do |key|
translations[key] = property_name
end
end
end
end
private
def define_translation(from, property_name, translator)
translations_hash[from][property_name] = translator
end
def define_writer_for_source_property(property)
define_method "#{property}=" do |val|
__translations[property].each do |name, with|
self[name] = with.respond_to?(:call) ? with.call(val) : val
end
end
end
def fail_self_transformation_error!(property_name)
raise ArgumentError,
"Property name (#{property_name}) and :from option must not be the same"
end
def valid_transformer?(transformer)
transformer.respond_to? :call
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)
super(property, value) if self.class.properties.include?(property)
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
private
def __translations
self.class.translations_hash
end
end
end
end
end
end