lib/activerecord-tableless.rb
# See #ActiveRecord::Tableless
require 'activerecord-tableless/version'
module ActiveRecord
# = ActiveRecord::Tableless
#
# Allow classes to behave like ActiveRecord models, but without an associated
# database table. A great way to capitalize on validations. Based on the
# original post at http://www.railsweenie.com/forums/2/topics/724 (which seems
# to have disappeared from the face of the earth).
#
# = Example usage
#
# class ContactMessage < ActiveRecord::Base
#
# has_no_table
#
# column :name, :string
# column :email, :string
# column :message, :string
#
# end
#
# msg = ContactMessage.new( params[:msg] )
# if msg.valid?
# ContactMessageSender.deliver_message( msg )
# redirect_to :action => :sent
# end
#
module Tableless
require 'active_record'
class NoDatabase < StandardError; end
class Unsupported < StandardError; end
def self.included( base ) #:nodoc:
base.send :extend, ActsMethods
end
module ActsMethods #:nodoc:
# A model that needs to be tableless will call this method to indicate
# it.
def has_no_table(options = {:database => :fail_fast})
raise ArgumentError.new("Invalid database option '#{options[:database]}'") unless [:fail_fast, :pretend_success].member? options[:database]
# keep our options handy
if ActiveRecord::VERSION::STRING < "3.1.0"
write_inheritable_attribute(:tableless_options,
{ :database => options[:database],
:columns => []
}
)
class_inheritable_reader :tableless_options
elsif ActiveRecord::VERSION::STRING >= "3.2.0"
class_attribute :tableless_options
self.tableless_options = {
:database => options[:database],
:columns => []
}
else
raise Unsupported.new("Sorry, ActiveRecord version #{ActiveRecord::VERSION::STRING} is not supported")
end
# extend
extend ActiveRecord::Tableless::SingletonMethods
extend ActiveRecord::Tableless::ClassMethods
# include
include ActiveRecord::Tableless::InstanceMethods
# setup columns
end
def tableless?
false
end
end
module SingletonMethods
# Return the list of columns registered for the model. Used internally by
# ActiveRecord
def columns
tableless_options[:columns]
end
# Register a new column.
if ActiveRecord::VERSION::STRING >= "4.2.0"
def column(name, sql_type = nil, default = nil, null = true)
cast_type = "ActiveRecord::Type::#{sql_type.to_s.camelize}".constantize.new
tableless_options[:columns] << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, cast_type, sql_type.to_s, null)
end
else
def column(name, sql_type = nil, default = nil, null = true)
tableless_options[:columns] << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null)
end
end
# Register a set of columns with the same SQL type
def add_columns(sql_type, *args)
args.each do |col|
column col, sql_type
end
end
def destroy(*args)
case tableless_options[:database]
when :pretend_success
self.new()
when :fail_fast
raise NoDatabase.new("Can't #destroy on Tableless class")
end
end
def destroy_all(*args)
case tableless_options[:database]
when :pretend_success
[]
when :fail_fast
raise NoDatabase.new("Can't #destroy_all on Tableless class")
end
end
case ActiveRecord::VERSION::MAJOR
when 3
def all(*args)
case tableless_options[:database]
when :pretend_success
[]
when :fail_fast
raise NoDatabase.new("Can't #find_every on Tableless class")
end
end
when 4
def find_by_sql(*args)
case tableless_options[:database]
when :pretend_success
[]
when :fail_fast
raise NoDatabase.new("Can't #find_by_sql on Tableless class")
end
end
else
raise Unsupported.new("Unsupported ActiveRecord version")
end
def transaction(&block)
# case tableless_options[:database]
# when :pretend_success
@_current_transaction_records ||= []
yield
# when :fail_fast
# raise NoDatabase.new("Can't #transaction on Tableless class")
# end
end
def tableless?
true
end
def table_exists?
false
end
end
module ClassMethods
def from_query_string(query_string)
unless query_string.blank?
params = query_string.split('&').collect do |chunk|
next if chunk.empty?
key, value = chunk.split('=', 2)
next if key.empty?
value = value.nil? ? nil : CGI.unescape(value)
[ CGI.unescape(key), value ]
end.compact.to_h
new(params)
else
new
end
end
def connection
conn = Object.new()
def conn.quote_table_name(*args)
""
end
def conn.quote_column_name(*args)
""
end
def conn.substitute_at(*args)
nil
end
def conn.schema_cache(*args)
schema_cache = Object.new()
def schema_cache.columns_hash(*args)
Hash.new()
end
schema_cache
end
conn
end
end
module InstanceMethods
def to_query_string(prefix = nil)
attributes.to_a.collect{|(name,value)| escaped_var_name(name, prefix) + "=" + escape_for_url(value) if value }.compact.join("&")
end
def quote_value(value, column = nil)
""
end
%w(create create_record _create_record update update_record _update_record).each do |method_name|
define_method(method_name) do |*args|
case self.class.tableless_options[:database]
when :pretend_success
true
when :fail_fast
raise NoDatabase.new("Can't ##{method_name} a Tableless object")
end
end
end
def destroy
case self.class.tableless_options[:database]
when :pretend_success
@destroyed = true
freeze
when :fail_fast
raise NoDatabase.new("Can't #destroy a Tableless object")
end
end
def reload(*args)
case self.class.tableless_options[:database]
when :pretend_success
self
when :fail_fast
raise NoDatabase.new("Can't #reload a Tableless object")
end
end
if ActiveRecord::VERSION::MAJOR >= 3
def add_to_transaction
end
end
private
def escaped_var_name(name, prefix = nil)
prefix ? "#{URI.escape(prefix)}[#{URI.escape(name)}]" : URI.escape(name)
end
def escape_for_url(value)
case value
when true then "1"
when false then "0"
when nil then ""
else URI.escape(value.to_s)
end
rescue
""
end
end
end
end
ActiveRecord::Base.send( :include, ActiveRecord::Tableless )