lib/locomotive/presentable.rb
module Locomotive
module Presentable
extend ActiveSupport::Concern
included do
# callbacks enabled
include ActiveSupport::Callbacks
define_callbacks :set_attributes
# keep tracks of the getters and setters
class << self; attr_accessor :getters, :setters, :property_options end
# __source is a reference to the main object (__ is for variable protection)
attr_reader :__source, :__options
end
# Initializer
def initialize(object, options = {})
@__source = object
@__options = options || {}
self.after_initialize
end
# Method called just after a presenter has been
# initialized. This method can be overridden.
#
def after_initialize
# do nothing
end
# Set the attributes of the presenter. Only call the methods
# which the presenter can handle.
#
# @param [ Hash ] value The attributes
#
def attributes=(values)
return unless values
@_attributes = values # memoize them for the callbacks
run_callbacks :set_attributes do
_values = values.stringify_keys
self.setters.each do |name|
if _values.has_key?(name)
_options = self.property_options[name] || {}
if _options[:if].blank? || self.instance_eval(&_options[:if])
self.send(:"#{name}=", _values[name])
end
end
end
end
end
# Build the hash of the values of all the properties.
#
# @param [ Array ] methods (Optional) List of methods instead of using the default getters
#
# @return [ Hash ] The "attributes" of the object
#
def as_json(methods = nil)
methods ||= self.getters
{}.tap do |hash|
methods.each do |meth|
_options = self.property_options[meth]
if _options[:if].blank? || self.instance_eval(&_options[:if])
value = self.send(meth.to_sym)
if !value.nil? || (_options && !!_options[:allow_nil])
hash[meth] = value
end
end
end
end
end
# Return the list of getters.
#
# @return [ List ] Array of method names
#
def getters
self.class.getters || []
end
# Return the list of setters.
#
# @return [ List ] Array of method names
#
def setters
self.class.setters || []
end
def property_options
self.class.property_options || {}
end
module ClassMethods
# Override inherited in order to copy the parent list
# of getters, setters and property options.
#
# @param [ Class ] subclass The subclass inheriting from the current class
#
def inherited(subclass)
subclass.getters = getters.clone if getters
subclass.setters = setters.clone if setters
subclass.property_options = property_options.clone if property_options
super
end
# Add multiple properties all in once.
# If th last property name is a hash, then it will be
# used as the options for all the other properties.
#
# @param [ Array ] args List of property names
#
def properties(*names)
options = names.last.is_a?(Hash) ? names.pop : {}
names.each do |name|
property(name, options)
end
end
# Add a property to the current instance. It creates getter/setter methods
# related to that property. By default, the getter and setter are bound
# to the source object.
#
# @param [ String ] name The name of the property
# @param [ Hash ] options The options related to the property.
#
def property(name, options = {})
aliases = [*options[:alias]]
collection = options[:collection] == true
(@property_options ||= {})[name.to_s] = options
unless options[:only_setter] == true
define_getter(name, collection)
end
unless options[:only_getter] == true
define_setter(name, aliases)
end
end
# Add a collection to the current instance. It creates getter/setter
# mapped to the collection of the source object.
#
# @param [ String ] name The name of the collection
# @param [ Hash ] options The options related to the collection (:alias)
#
def collection(name, options = {})
property(name, options.merge(collection: true, type: 'Array'))
end
def define_getter(name, collection = false)
(@getters ||= []) << name.to_s
class_eval <<-EOV
def #{name}
if #{collection.to_s}
list = self.__source.send(:#{name})
list ? list.map(&:as_json) : []
else
self.__source.send(:#{name})
end
end
EOV
end
def define_setter(name, aliases = [])
(@setters ||= []) << name.to_s
@setters += aliases.map(&:to_s)
class_eval <<-EOV
def #{name}=(value)
self.__source.send(:#{name}=, value)
end
EOV
aliases.each do |_name|
class_eval <<-EOV
def #{_name}=(value)
self.#{name} = value
end
EOV
end
end
# Get the name of the property for which
# the property passed in parameter is an alias.
#
# @param [ String ] alias_name Name of the alias
#
# @return [ String ] The original property
#
def alias_of(alias_name)
self.setters.find do |name|
list = [*(self.property_options[name] || {})[:alias]] || []
list.include?(alias_name.to_sym)
end
end
end # ClassMethods
end # Presentable
end # Locomotive