# encoding: utf-8
module CouchRest
module Model
module Properties
extend ActiveSupport::Concern
included do
class_attribute(:properties) unless self.respond_to?(:properties)
class_attribute(:properties_by_name) unless self.respond_to?(:properties_by_name)
self.properties ||= []
self.properties_by_name ||= {}
end
# Provide an attribute hash ready to be sent to CouchDB but with
# all the nil attributes removed.
def as_couch_json
super.delete_if{|k,v| v.nil?}
end
# Read the casted value of an attribute defined with a property.
def read_attribute(property)
self[find_property!(property).to_s]
end
# Store a casted value in the current instance of an attribute defined
# with a property.
def write_attribute(property, value)
prop = find_property!(property)
value = prop.cast(self, value)
self[prop.name] = value
end
# Returns a hash of this object's attributes with a defined property.
# This is effectively an accessor to the underlying CouchRest
# attributes hash.
def read_attributes
@_attributes
end
alias :attributes :read_attributes
# Takes a hash as argument, and applies the values by using writer
# methods respecting protected properties.
def write_attributes(hash)
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
self
end
alias :attributes= :write_attributes
# Takes the provided attribute hash and sets all properties, assuming
# that the data is from a trusted source, such as the database.
def write_all_attributes(attrs = {})
directly_set_read_only_attributes(attrs)
directly_set_attributes(attrs, true)
self
end
protected
def find_property(property)
property.is_a?(Property) ? property : self.class.properties_by_name[property.to_s]
end
def find_property!(property)
find_property(property) or
raise ArgumentError, "Missing property definition for #{property.to_s}"
end
def write_attributes_for_initialization(attrs = {}, opts = {})
apply_all_property_defaults
if opts[:write_all_attributes]
# Assume coming from a database, so we clear change information after
write_all_attributes(attrs)
clear_changes_information
else
# Not from a persisted source, clear the change data in advance and do
# not set protected or read-only attributes.
clear_changes_information
write_attributes(attrs)
end
end
# Apply each property's default value to the attributes. This should
# only ever be called on initialization.
def apply_all_property_defaults
self.class.properties.each do |property|
write_attribute(property, property.default_value)
end
end
# Set all the attributes and return a hash with the attributes
# that have not been accepted.
def directly_set_attributes(hash, mass_assign = false)
return if hash.nil?
multi_parameter_attributes = []
hash.reject do |key, value|
if key.to_s.include?("(")
multi_parameter_attributes << [ key, value ]
false
elsif self.respond_to?("#{key}=")
self.send("#{key}=", value)
elsif mass_assign || mass_assign_any_attribute
self[key] = value
end
end
# Handle attributes provided in an embedded object format, such
# as a web-form.
unless multi_parameter_attributes.empty?
assign_multiparameter_attributes(multi_parameter_attributes, hash)
end
end
def directly_set_read_only_attributes(hash)
property_list = self.properties.map{|p| p.name}
hash.each do |attribute_name, attribute_value|
next if self.respond_to?("#{attribute_name}=")
if property_list.include?(attribute_name)
write_attribute(attribute_name, hash.delete(attribute_name))
end
end
end
def assign_multiparameter_attributes(pairs, hash)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs), hash
)
end
def execute_callstack_for_multiparameter_attributes(callstack, hash)
callstack.each do |name, values_with_empty_parameters|
if self.respond_to?("#{name}=")
casted_attrib = send("#{name}=", values_with_empty_parameters)
unless casted_attrib.is_a?(Hash)
hash.reject { |key, value| key.include?(name.to_s)}
end
end
end
hash
end
def extract_callstack_for_multiparameter_attributes(pairs)
attributes = { }
pairs.each do |pair|
multiparameter_name, value = pair
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
attributes[attribute_name][find_parameter_name(multiparameter_name)] ||= value
end
attributes
end
def find_parameter_name(multiparameter_name)
position = multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
{1 => :year, 2 => :month, 3 => :day, 4 => :hour, 5 => :min, 6 => :sec}[position]
end
module ClassMethods
def property(name, *options, &block)
raise "Invalid property definition, '#{name}' already used for CouchRest Model type field" if name.to_s == model_type_key.to_s && CouchRest::Model::Base >= self
opts = { }
type = options.shift
if type.class != Hash
opts[:type] = type
opts.merge!(options.shift || {})
else
opts.update(type)
end
existing_property = self.properties.find{|p| p.name == name.to_s}
if existing_property.nil? || (existing_property.default != opts[:default])
define_property(name, opts, &block)
end
end
# Automatically set updated_at and created_at fields
# on the document whenever saving occurs.
#
# These properties are casted as Time objects, so they should always
# be set to UTC.
def timestamps!
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object|
write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new?
end
end
protected
# This is not a thread safe operation, if you have to set new properties at runtime
# make sure a mutex is used.
def define_property(name, options = {}, &block)
property = Property.new(name, options, &block)
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
if property.type.respond_to?(:validates_casted_model)
validates_casted_model property.name
end
# Dirty!
create_dirty_property_methods(property)
properties << property
properties_by_name[property.to_s] = property
property
end
# defines the getter for the property (and optional aliases)
def create_property_getter(property)
define_method(property.name) do
read_attribute(property.name)
end
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
define_method("#{property.name}?") do
value = read_attribute(property.name)
!(value.nil? || value == false)
end
end
if property.alias
alias_method(property.alias, property.name.to_sym)
end
end
# defines the setter for the property (and optional aliases)
def create_property_setter(property)
name = property.name
define_method("#{name}=") do |value|
write_attribute(name, value)
end
if property.alias
alias_method "#{property.alias}=", "#{name}="
end
end
end # module ClassMethods
end
end
end