# download @ http://s3.amazonaws.com/drawohara.com.ruby/properties.rb
# view direct @ http://s3.amazonaws.com/drawohara.com.snippets/properties_rb.html
#

# ActiveRecord accessor methods are not really dynamic - rather they
# dynamically get/set static properites as defined by the database.
# 
# properties.rb is a layer on top of ActiveRecord which allows arbitrary named
# and typed properties to be associated with any AR object in true dynamic
# fashion.  this is useful when arbitrary information needs to be associated
# with an object or when rapid development needs to be done without the pain
# of dicking with database evertime a property needs to be added to an object.
# this means you can use something like the following on *any* model
#
#   class Model < ActiveRecord::Base
#     properties(
#       :a => :float,
#       :b => :uri,
#       :c => { :value => 42, :cast => :integer }
#     )
#   end
#
#   m = Model.new
#
#   p m.properties[:a] #=> nil
#   p m.properties[:b] #=> nil
#   p m.properties[:c] #=> 42 
#
#   p m.a #=> nil
#   p m.b #=> nil
#   p m.c #=> 42 
#
#   m.properties[:b] = 'http://codeforpeople.com'
#
#   p m.properties[:b] #=> URI('http://codeforpeople.com')
#
#
# the 'properties' class method will add a polymorphic has_many association
# named 'property_records' to the target class.  you shouldn't need to deal
# with this association directly but you can if you think about what your're
# doing.  in general though, accessing that collection via the 'properties'
# instance method will be much more convenient.  for instance, that method
# will ensure that any declared properties are created for each new or loaded
# object - notice how all the properites :a, :b, and :c were pre-populated in
# the above example.  it will also transparently make sure properties are
# either created or updated where appropriate. you can always add properties
# to any object
#
#   m.properties.add 'arbitrary' => :float 
#
#   m.properties.add 'foobar' => { :cast => :integer, :value => 42 } 
#
# and these will be available on subsequent loads of the object.  however, if
# you set
#
#   class Model
#     properties.strict true
#   end
#
# then *only* those properties declared at the class scope will be loaded and
# any stray properties will be destroyed when the object is loaded.  in
# otherwords
#
#   class Model < ActiveRecord::Base
#     properties(
#       :a => :float,
#       :b => :uri
#     )
#   end
#
# means all Model objects will have *at least* :a and :b properites - you'll
# be able to add arbitrary properties to individual model objects.  while
#
#   class Model < ActiveRecord::Base
#     properties(
#       :a => :float,
#       :b => :uri
#     )
#
#     properties.strict
#   end
#
# means that all models will have *only* those properties - no additional
# onces may be added to instance records
#
# properites inherit to subclasses, and subclasses can add properties in the
# POLS fashion
#
#   class A < ActiveRecord::Base
#     properites :a => string
#   end
#
#   class A < B 
#     properites :b => string
#   end
#
#   B.new.properties.keys #=> :a, :b
#
# you'll note that the properties container has a lot of hash-like methods
# such as
# 
#   #values
#   #keys
#   #clear
#   #to_hash
#   #update
#
# and many more
#
# use properites when you require a variable collection of things to be
# associated with a record, such as configuration information for
# active_merchant gateways, dimensions to n-dimensional objects, etc.  it's
# useful anytime where objects need an arbitrary set of properites in addition
# to the normal strict static set of boring columns and relationships.
#
# to get using properties.rb just drop it in your RAILS_ROOT/lib/ directory
# and run the migration using
#
#   ./script/runner ' Properties.migrate '
#
# by default this uses a table name :properites, you can supply your own name
# using
#
#   ./script/runner ' Properties.table_name = :props and Properties.migrate  '
#
# if you choose to do this you'll also need to add
#
#   Properites.table_name = :props
#
# somewhere in your code like config/environment.rb or config/initializers/*
#
# enjoy.
#

module ActiveRecord
  module Properties


  # the class level dsl for declaring properties
  #
    module ClassMethods

    # declare properties, return the map of properties for this class
    # hierarchy
    #
      def properties description = {}
      # setup the properites table for this and all subclasses
      #
        @@properties ||= Table.new

      # return it iff asked to
      #
        return @@properties if description == :table

      # bootstrap property instance methods based on descriptions
      #
        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

      # associate the property collection
      #
        has_many(:property_records,
          :as => :propertied,
          :class_name => Properties::Model.name,
          :dependent => :destroy,
          :uniq => true
        )

      # make sure all new records will get properties, with their default
      # filled in
      #
        after_initialize :properties
        after_initialize :initialize_from_properties

      # add the properties collection wrapper method
      #
        include IntanceMethods

        return @@properties
      end

      alias_method :has_properties, :properties
    end


  # the instance level methods added on demand by the class level dsl
  #
    module IntanceMethods

    # wrap the property_records collection in a Map to manage the associated
    # records in a grok'able way
    #
      def properties options = {}
        options.to_options!

        if(options[:reload] or not defined?(@properties) or @properties.nil?)
          @properties = Map.for self
        end

        @properties
      end
    end

  # the associative array of klass -> properties for a class hierarchy
  #
    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


  # the wrapper for the collection that makes everything sugar and spice
  #
    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

    # bootstrap the object by walking down all appropriate initializers in the
    # class hierarchy - we'll end up with all properties from all superclasses
    #
      def initialize_from_class_properties!
        strict = table.strict
        table.strict = false

        table.each do |pair|
        # initialize from all superclasses
        #
          klass, description = pair
          next unless @object.class <= klass

        # make sure we have all the properties we should
        #
          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

        # and none of the ones we should not iff we are being strict
        #
          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

    # this let's you not worry if a property accociated record exists and
    # needs updated, or needs created - this abstraction lets you just add
    # them and nothing bad happens when they already exist
    #
      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


  # the migration - copy something similar to your db/migrate dir if you like
  #
    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


  # the casts supported - note that you may add your own to the index -
  # anything that responds to 'call' will do
  #
    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


  # the polymorphically associated model - this stores all properties for all
  # tables.  it's also responsible for applying casts to the values on
  # read/write
  #
    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

# bless AR with the properties dsl
#
  ActiveRecord::Base.send :include, ActiveRecord::Properties