lib/blendris/model.rb in blendris-0.0.2 vs lib/blendris/model.rb in blendris-0.0.3
- old
+ new
@@ -1,72 +1,112 @@
module Blendris
+ # Model is the main driver for Blendris. All Blendris objects
+ # will inherit from it to function as a database model.
+
class Model
include RedisAccessor
attr_reader :key
+ # Instantiate a new instance of this model. We do some basic
+ # checking to make sure that this object already exists in Redis
+ # as the requested type. This is to prevent keys being used in
+ # the wrong way.
+
+ # If the :verify option isn't set to false, then each field of
+ # this model is also verified.
+
def initialize(new_key, options = {})
@key = sanitize_key(new_key)
- actual_type = constantize(redis.get(prefix + key))
+ actual_type = constantize(redis.get(key))
raise ArgumentError.new("#{self.class.name} second argument must be a hash") unless options.kind_of? Hash
- raise TypeError.new("#{prefix + key} does not exist, not a #{self.class.name} - you may want create instead of new") if !actual_type
- raise TypeError.new("#{prefix + key} is a #{actual_type}, not a #{self.class.name}") if actual_type != self.class
+ raise TypeError.new("#{key} does not exist, not a #{self.class.name} - you may want create instead of new") if !actual_type
+ raise TypeError.new("#{key} is a #{actual_type}, not a #{self.class.name}") if actual_type != self.class
if options[:verify] != false
parameters = self.class.local_parameters.find_all {|s| s.kind_of? Symbol}
dne = parameters.find {|p| not self.send(p.to_s)}
raise ArgumentError.new("#{self.class.name} #{key} is missing its #{dne}") if dne
raise ArgumentError.new("blank keys are not allowed") if @key.length == 0
end
end
+ # An object's id is considered to be the SHA1 digest of its key. This is
+ # to ensure that all objects that represent the same key return the same id.
def id
Digest::SHA1.hexdigest key
end
+ # TODO: Create the methods in the initialize method instead of depending
+ # on method_missing to dispatch to the correct methods. This will make
+ # these objects better for mocking and stubbing.
def method_missing(method_sym, *arguments)
(name, setter) = method_sym.to_s.scan(/(.*[^=])(=)?/).first
if node = redis_symbol(name)
if setter
- return node.set *arguments
+ return node.set(*arguments)
else
return node.get
end
end
super
end
+ # Look up the given symbol by its name. The list of symbols are defined
+ # when the model is declared.
+ # TODO: This can also probably go away when I remove the need for method_missing.
def redis_symbol(name)
subkey = self.subkey(name)
options = self.class.redis_symbols[name.to_s]
return unless options
- options = options.merge(:model => self)
+ on_change = lambda { self.fire_on_change_for name }
+ options = options.merge(:model => self, :on_change => on_change)
options[:type].new subkey, options
end
- def subkey(key)
- sanitize_key "#{self.key}:#{key}"
+ # Calculate the key to address the given child node.
+ def subkey(child)
+ sanitize_key "#{self.key}:#{child}"
end
+ # Compare two instances. If two instances have the same class and key, they are equal.
def ==(other)
return false unless self.class == other.class
return self.key == other.key
end
+ # Return a list of field names for this model.
+ def fields
+ self.class.redis_symbols.map {|name, field| name.to_s}
+ end
+
+ # Fire the list of blocks called when the given symbol changes.
+ def fire_on_change_for(symbol)
+ blocks = [ self.class.on_change_table[nil], self.class.on_change_table[symbol.to_s] ]
+
+ blocks.flatten!
+ blocks.compact!
+
+ blocks.each do |block|
+ self.instance_exec symbol.to_s, &block
+ end
+ end
+
class << self
include RedisAccessor
+ include Enumerable
# This method will instantiate a new object with the correct key
# and assign the values passed to it.
def create(*args)
parameters = local_parameters.find_all {|s| s.kind_of? Symbol}
@@ -77,17 +117,18 @@
msg = "wrong number of arguments for a new #{self.class.name} (%d for %d)" % [ got, wanted ]
raise ArgumentError.new(msg)
end
key = generate_key(self, args)
- current_model = redis.get(prefix + key)
+ current_model = redis.get(key)
if current_model && current_model != self.name
raise ArgumentError.new("#{key} is a #{current_model}, not a #{self.name}")
end
- redis.set(prefix + key, self.name)
+ redis.set key, self.name
+ redis.sadd index_key, key
obj = new(key, :verify => false)
parameters.each_with_index do |parm, i|
obj.redis_symbol(parm).set args[i]
@@ -103,10 +144,18 @@
@local_parameters.compact!
nil
end
+ def each
+ RedisSet.new(index_key).each {|k| yield new(k)}
+ end
+
+ def index_key
+ "index:model:#{self.name}"
+ end
+
# Defines a new data type for Blendris:Model construction.
def type(name, klass)
(class << self; self; end).instance_eval do
define_method(name) do |*args|
varname = args.shift.to_s
@@ -132,9 +181,30 @@
def cast_value(symbol, value)
options = redis_symbols[symbol.to_s]
raise ArgumentError.new("#{self.name} is missing its #{symbol}") unless options
options[:type].cast_to_redis value, options
+ end
+
+ # Define a block to call when one of the given symbol values changes.
+ def on_change(*symbols, &block)
+ symbols.flatten!
+ symbols.compact!
+
+ if symbols.count == 0
+ on_change_table[nil] ||= []
+ on_change_table[nil] << block
+ else
+ symbols.each do |symbol|
+ on_change_table[symbol.to_s] ||= []
+ on_change_table[symbol.to_s] << block
+ end
+ end
+ end
+
+ # The hash of blocks called when fields on this object change.
+ def on_change_table
+ @on_change_table ||= {}
end
end
end