1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
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.
#
# * <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
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)
if self.class.transformation_exists? property
super property, self.class.transformed_property(property, value)
elsif self.class.properties.include?(property)
super(property, value)
end
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
|