module ActiveRecord
module Properties
module ClassMethods
def properties description = {}
@@properties ||= Table.new
return @@properties if description == :table
klass = self
unless description.empty?
if pair = @@properties.assoc(klass)
pair.last.update description
else
@@properties << [klass, description]
end
description.each do |lhs, rhs|
lhs = lhs.to_s
case rhs
when Hash
rhs = rhs.to_options
key = rhs[:key] || lhs
else
key = lhs
end
key = key.to_s
code = <<-code
def #{ key }()
properties[#{ key.inspect }]
end
def #{ key }= value
properties[#{ key.inspect }] = value
end
code
module_eval code
end
code = <<-code
def initialize_from_properties
@attributes.reverse_merge! properties.to_hash
end
begin
instance_method(:after_initialize)
rescue Object
def after_initialize(*args, &block) super if defined?(super) end
end
code
module_eval code
end
has_many(:property_records,
:as => :propertied,
:class_name => Properties::Model.name,
:dependent => :destroy,
:uniq => true
)
after_initialize :properties
after_initialize :initialize_from_properties
include IntanceMethods
return @@properties
end
alias_method :has_properties, :properties
end
module IntanceMethods
def properties options = {}
options.to_options!
if(options[:reload] or not defined?(@properties) or @properties.nil?)
@properties = Map.for self
end
@properties
end
end
class Table < ::Array
def strict= mode
@strict = mode
end
def strict *mode
self.strict = mode.first unless mode.empty?
@strict
end
def for klass
assoc klass
end
end
class Map
def Map.for object
new object
end
def initialize object, options = {}
@object = object
initialize_from_class_properties!
end
def collection
@object.property_records
end
def table
@table ||= @object.class.properties(:table)
end
def initialize_from_class_properties!
strict = table.strict
table.strict = false
table.each do |pair|
klass, description = pair
next unless @object.class <= klass
allowed = []
description.each do |lhs, rhs|
lhs = lhs.to_s
allowed << lhs
case rhs
when Hash
rhs = rhs.to_options
create_unless_exists(rhs.update(:key => lhs))
else
create_unless_exists({ :key => lhs, :cast => rhs.to_s })
end
end
if table.strict
need_to_reload = false
collection.each do |record|
unless allowed.include?(record.key.to_s)
record.destroy
need_to_reload = true
end
end
collection.reload if need_to_reload
end
end
ensure
table.strict = strict
end
def create_unless_exists *args
raise Error, "you cannot add properties (strict mode)" if table.strict
args.map! do |arg|
case arg
when Hash
arg.to_options!
key = arg.delete :key
value = arg.delete :value
cast = arg.delete :cast
if key.nil?
key, cast = arg.to_a.first
end
{ :key => key.to_s, :value => value, :cast => cast.to_s }
else
{ :key => arg.to_s }
end
end
records = []
msg = @object.new_record? ? 'build' : 'create'
args.each do |hash|
hash.to_options!
key = hash[:key]
record = nil
unless has_property?(key)
begin
record = collection.send(msg, hash)
records << record
rescue Object
raise unless has_property?(key)
end
end
end
records
end
alias_method 'add', 'create_unless_exists'
def has_property? key
collection.any?{|record| record.key.to_s == key.to_s}
end
def to_hash
hash = HashWithIndifferentAccess.new
collection.inject hash do |hash, record|
hash.update record.key => record.value
end
end
alias_method 'to_h', 'to_hash'
def each &block
collection.each do |record|
block.call record.key, record.value
end
end
alias_method 'each_pair', 'each'
def map &block
collection.map do |record|
block.call record.key, record.value
end
end
def map! &block
collection.map do |record|
key, value, *ignored = block.call record.key, record.value
record.key = key
record.value = value
record
end
end
def inspect
to_hash.inspect
end
def clear
collection.clear
end
def keys
collection.map &:key
end
def has_key? key
collection.detect{|r| r.key == key.to_s}
end
def values
collection.map &:value
end
def to_a
to_hash.to_a
end
def [] *keys
values =
keys.flatten.map do |key|
key = key.to_s
record = collection.detect{|record| record.key == key}
raise ActiveRecord::RecordNotFound unless record
record.value
end
((keys.first.is_a?(Array) or keys.size > 1) ? values : values.first)
end
def update kvs = {}
kvs.each do |key, value|
key = key.to_s
record = collection.detect{|record| record.key == key}
raise ActiveRecord::RecordNotFound unless record
record.value = value
end
self
end
def []= key, value
update key => value
value
end
end
class Migration < ActiveRecord::Migration
def Migration.up
create_table Properties.table_name, :force => true do |t|
t.string :key; t.string :value; t.string :cast;
t.references :propertied, :polymorphic => true
end
add_index Properties.table_name, [:propertied_type, :propertied_id, :key], :unique => true
add_index Properties.table_name, [:propertied_type, :propertied_id]
add_index Properties.table_name, [:propertied_type]
end
def Migration.down
remove_index Properties.table_name, [:propertied_type]
remove_index Properties.table_name, [:propertied_type, :propertied_id]
remove_index Properties.table_name, [:propertied_type, :propertied_id, :key]
drop_table Properties.table_name
end
end
def Properties.table_name
@table_name ||= 'properties'
end
def Properties.table_name= table_name
@table_name = table_name
end
def Properties.migrate direction = 'up'
Migration.send direction
end
def Properties.included other
other.send :extend, ClassMethods
end
class Error < ::StandardError; end
module Cast
def for cast
cast ||= 'string'
cast = cast.to_s.downcase.strip
if index[cast]
index[cast]
else
index[cast] =
begin
method(cast).to_proc
rescue Object
raise Error, "no such cast #{ cast }"
end
end
end
def index
@index ||= HashWithIndifferentAccess.new
end
def [] cast, callable
index[cast] = callable
end
def integer value
Float(value.to_s).to_i
end
def float value
Float(value.to_s)
end
def boolean value
case value.to_s
when %r/^false|f|0/i
false
else
true
end
end
def maybe value
case value.to_s
when %r/^nil|unknown|maybe$/i
nil
when %r/^false|f|0/i
false
else
true
end
end
def date value
require 'date'
Date.parse value.to_s
end
def time value
require 'time'
Time.parse value.to_s
end
def datetime value
require 'datetime'
DateTime.parse value.to_s
end
def uri value
require 'uri'
URI.parse value.to_s
end
def symbol value
value.to_s.to_sym
end
def string value
value.to_s
end
extend self
end
class Model < ::ActiveRecord::Base
set_table_name Properties.table_name
belongs_to :propertied, :polymorphic => true
def value
return nil if self['value'].to_s.empty?
Cast.for(cast)[ self['value'] ]
end
def value= value
self['value'] = Cast.for(cast)[ value ]
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord::Properties