Skip to content

Instantly share code, notes, and snippets.

@actsasflinn
Created August 10, 2009 03:39
Show Gist options
  • Save actsasflinn/165002 to your computer and use it in GitHub Desktop.
Save actsasflinn/165002 to your computer and use it in GitHub Desktop.
TyrantRecord, an experimental interface for modeling using Tokyo Tyrant
# Tyrannical
require 'tokyo_tyrant'
class TrueClass
def to_tokyo_tyrant;"1";end
end
class FalseClass
def to_tokyo_tyrant;"0";end
end
class Time
def to_tokyo_tyrant;to_i.to_s;end
end
class Tyrannical
include ActiveModel::Validations
# Stores the default scope for the class
class_inheritable_accessor :connection
self.connection = TokyoTyrant::Table.new
class_inheritable_accessor :type_casts
self.type_casts = {
:integer => proc{ |v| Integer(v) },
:float => proc{ |v| Float(v) },
:time => proc{ |v| Time.at(v.to_i) },
:date => proc{ |v| Date.parse(v) },
:boolean => proc{ |v| v == "0" ? false : true },
:raw => proc{ |v| Marshal.load(v) }
}
class_inheritable_accessor :columns
self.columns = {}
class_inheritable_accessor :protected_attributes
self.protected_attributes = [:__id, :type]
attr_accessor :attributes
@attributes = {}
attr_accessor :new_record
def initialize(attributes = {})
@attributes = attributes
@new_record = true
set_defaults
end
def set_defaults
self.class.columns.each{ |name, options|
if attributes[name].blank?
if options[:default].respond_to?(:call)
attributes[name] = options[:default].call
else
attributes[name] = options[:default]
end
end
}
end
def cast_types
self.class.columns.each{ |name, options|
if attributes[name] && options[:type].respond_to?(:call)
attributes[name] = options[:type].call(attributes[name])
end
}
end
def new_record?
@new_record
end
def [](k)
attributes[k.to_sym]
end
def []=(k,v)
attributes[k.to_sym] = v
end
def to_h
attributes
end
def id
attributes[:id] ||= connection.genuid
end
def key
@key ||= self.class.key(id)
end
def save
return false unless valid?
value = self.to_h.merge(:type => self.class.type_name)
self.new_record = !connection.put(key, value)
self
end
def method_missing(name, *args, &block)
attribute_name = name.to_s.gsub(/=$/,'').to_sym
if attributes.include?(attribute_name)
if name.to_s.match(/=$/)
self.class.define_writer(attribute_name)
else
self.class.define_reader(attribute_name)
end
send(name, *args)
else
super
end
end
class << self
def instantiate(h)
object = allocate
h.symbolize_keys!
protected_attributes.each{ |k| h.delete(k) }
object.instance_variable_set("@attributes", h)
object.instance_variable_set("@new_record", false)
object.set_defaults
object.cast_types
object
end
def create(attributes = {})
object = new(attributes)
object.save
object
end
# Define an attribute
# attribute(:foo)
# attribute(:foo, :default => "Bar")
# attribute(:foo, :integer)
# attribute(:foo, :type => integer, :default => 1)
# attribute(:foo, :type => :raw)
# attribute(:foo, :type => proc{ |v| Thing.from_string(v) })
# attribute(:foo){ |v| Thing.from_string(v) }
#
def attribute(name, options = {}, &block)
cast, options = options.is_a?(Hash) ? [options.delete(:type), options] : [options, {}]
if block_given?
options[:type] = block
elsif cast.is_a?(Symbol) && type_casts.include?(cast)
options[:type] = type_casts[cast]
end
columns[name] = options
# this will need to be a little more formal
define_reader(name)
define_writer(name)
end
def define_reader(name)
define_method(name){ attributes[name] } unless method_defined?(name)
end
def define_writer(name)
writer_name = "#{name}=".to_sym
define_method(writer_name){ |value| attributes[name] = value } unless method_defined?(writer_name)
end
def type_name
@type_name ||= name.tableize
end
def key(id)
"#{type_name}/#{id}"
end
def get(k)
if res = connection.get(key(k))
instantiate(res)
end
end
alias_method :[], :get
def get!(k)
if res = get(k)
res
else
raise "No such record"
end
end
def all(keys = [], &block)
if block_given? || keys.size.zero?
results = connection.prepare_query(&block).condition(:type, :streq, type_name).get
else
keys = keys.collect{ |key| key(key) }
results = connection.mget(keys)
ordered_results = keys.inject([]){ |res, k| results[k] ? res << results[k] : res }
results = ordered_results
end
results.collect{ |object| instantiate(object) }
end
def count(&block)
connection.prepare_query(&block).condition(:type, :streq, type_name).count
end
def destroy(keys = [], &block)
if block_given?
connection.prepare_query(&block).condition(:type, :streq, type_name).delete
else
keys.each{ |key| connection.delete(key(key)) }
end
end
def destroy_all
connection.query.condition(:type, :streq, type_name).delete
end
end
end
class Thing
attr_accessor :one
@one = ""
attr_accessor :two
@two = ""
def initialize(one, two)
@one, @two = one, two
end
def to_s
"#{one}:#{two}"
end
def self.from_string(s)
new(*s.split(":"))
end
end
class Example < Tyrannical
attribute(:id, :integer)
attribute(:num, :integer)
attribute(:starts_at, :type => :time, :default => proc{ Time.now })
attribute(:thing){ |v| Thing.from_string(v) }
attribute(:stuff, :type => :raw)
validates_presence_of :num
validates_numericality_of :num
# Not implemented yet
def before_save
attributes[:stuff] = Marshal.dump(attributes[:stuff])
end
end
e = Example.create(:name => 'Foo', :starts_at => Time.now, :thing => Thing.new("foo", "bar"))
=> #<Example:0x24291ac @attributes={:starts_at=>Mon Aug 24 00:52:44 -0400 2009, :name=>"Foo", :thing=>#<Thing:0x24293b4 @one="foo", @two="bar">, :id=>9}, @new_record=false>
>> e = Example[9]
=> #<Example:0x240d844 @attributes={:starts_at=>Mon Aug 24 00:52:44 -0400 2009, :name=>"Foo", :thing=>#<Thing:0x240cfac @one="foo", @two="bar">, :id=>9}, @new_record=false>
>> Tyrannical.connection[e.key]
=> {"name"=>"Foo", "type"=>"examples", "id"=>"9", "starts_at"=>"1251089564", "thing"=>"foo:bar"}
Tyrannical.connection.keys
# => ["examples/9"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment