Skip to content
Browse files

begin implementing attribute DSL

* Only supporting top-level keys currently, using dotted keys will result in unexpected/untested behavior
* Supports getter/setter method for each attribute 
* Supports optional type casting on-load and when setting values (though no type checking). nil values are ignored in casts
  • Loading branch information...
1 parent 527f586 commit 8f2b0507fb2dea10167e6a76f7cb4e408519f365 @jrwest jrwest committed May 29, 2011
View
3 lib/mongomatic.rb
@@ -9,6 +9,7 @@
require "#{File.dirname(__FILE__)}/mongomatic/m_hash"
require "#{File.dirname(__FILE__)}/mongomatic/errors"
require "#{File.dirname(__FILE__)}/mongomatic/cursor"
+require "#{File.dirname(__FILE__)}/mongomatic/attributes"
module Mongomatic
@@ -30,6 +31,7 @@ def self.db=(obj)
def self.included(klass)
klass.send(:include, InstanceMethods)
klass.send(:extend, ClassMethods)
+ klass.send(:include, Attributes)
end
end
@@ -40,7 +42,6 @@ def self.included(klass)
require "#{File.dirname(__FILE__)}/mongomatic/modifiers"
require "#{File.dirname(__FILE__)}/mongomatic/expectations"
require "#{File.dirname(__FILE__)}/mongomatic/active_model_compliancy"
-require "#{File.dirname(__FILE__)}/mongomatic/type_converters"
require "#{File.dirname(__FILE__)}/mongomatic/typed_fields"
require "#{File.dirname(__FILE__)}/mongomatic/base"
require "#{File.dirname(__FILE__)}/mongomatic/transaction_lock"
View
95 lib/mongomatic/attributes.rb
@@ -0,0 +1,95 @@
+require "#{File.dirname(__FILE__)}/type_converters"
+
+module Mongomatic
+ module Attributes
+
+ def self.included(klass)
+ klass.send(:extend, ClassMethods)
+ end
+
+ def initialize(doc_hash=Mongomatic::MHash.new, is_new=true)
+ super(doc_hash, is_new)
+ cast_attributes
+ end
+
+ def []=(key, value)
+ super(key, cast_attribute(key.to_s, value))
+ end
+
+ def set_value_for_key(key, value)
+ super(key, cast_attribute(key.to_s, value))
+ end
+
+ def attributes
+ self.class.attributes
+ end
+
+ def attribute_data
+ self.class.attribute_data
+ end
+
+ def cast_attributes
+ self.attribute_data.keys.each do |attr|
+ cast_attribute(attr, self[attr])
+ end
+ end
+
+ def cast_attribute(attr, val, update=true)
+ return val unless self.attribute_data[attr]
+
+ type = self.attribute_data[attr][:typed]
+ converter_klass = TypeConverters.for_type(type)
+ return val unless converter_klass
+
+ converter = converter_klass.new(val)
+ return val if converter.type_match?
+
+ cast_val = converter.cast
+ self.doc[attr] = cast_val
+
+ cast_val
+ end
+
+ module ClassMethods
+
+ def attribute(name, opts = {})
+ init_attributes
+ define_attribute_methods(name) unless @attributes[name]
+ @attributes[name.to_s] = opts
+ end
+
+ def attributes
+ init_attributes
+ @attributes.keys.map(&:to_sym)
+ end
+
+ def attribute_data
+ init_attributes
+ @attributes
+ end
+
+ def init_attributes
+ @attributes ||= {}
+ end
+ private :init_attributes
+
+ def define_attribute_methods(name)
+ self.instance_eval do
+
+ # define reader for attribute
+ define_method(name) do
+ self[name]
+ end
+
+ # define writer for attribute
+ define_method("#{name}=") do |val|
+ self[name] = val
+ end
+
+ end
+ end
+ private :define_attribute_methods
+
+ end
+ end
+end
View
5 lib/mongomatic/instance_methods.rb
@@ -1,6 +1,9 @@
module Mongomatic
module InstanceMethods
def self.included(klass)
+ klass.send(:public, :initialize)
+ klass.send(:public, :set_value_for_key)
+ klass.send(:public, :[]=)
klass.send(:attr_accessor, :removed, :is_new, :errors)
klass.send(:attr_reader, :doc)
klass.send(:include, Modifiers)
@@ -15,6 +18,7 @@ def initialize(doc_hash=Mongomatic::MHash.new, is_new=true)
self.errors = Mongomatic::Errors.new
do_callback(:after_initialize)
end
+ module_function :initialize
# Insert the document into the database. Will return false if the document has
# already been inserted or is invalid. Returns the generated BSON::ObjectId
@@ -160,6 +164,7 @@ def set_value_for_key(key, value)
val = value.kind_of?(Hash) ? Mongomatic::MHash.new(value) : value
res[field_accessor] = val
end
+ module_function :set_value_for_key
alias :[]= :set_value_for_key
# Merge this document with the supplied hash. Useful for updates:
View
27 lib/mongomatic/type_converters.rb
@@ -3,7 +3,29 @@ module TypeConverters
class CannotCastValue < RuntimeError; end
def self.for_type(type)
- eval "Mongomatic::TypeConverters::#{type.to_s.camelize}"
+ return nil unless type
+ type_s = type.to_s
+ type_val = type_s =~ /::/ ? type_s.split(/::/).last.to_sym : type_s.to_sym
+ if check_const(type_val)
+ self.get_const(type_val)
+ end
+ end
+
+ def self.check_const(type)
+ if RUBY_VERSION =~ /^1\.8/
+ self.const_defined?(type)
+ else
+ self.const_defined?(type, false)
+ end
+ end
+
+ def self.get_const(type)
+ if RUBY_VERSION =~ /^1\.8/
+ self.const_get(type)
+ else
+ self.const_get(type, false)
+ end
+
end
class Base
@@ -16,6 +38,7 @@ def type_match?
end
def cast
+ return nil unless @orig_val
if type_match?
@orig_val
else
@@ -143,4 +166,4 @@ def convert_orig_val
end
end
-end
+end
View
58 spec/mongomatic/attributes_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe "Mongomatic Attributes" do
+ subject { GameObject.new(:silver_count => 2, :gold_count => "2", :friends => ["Tariq", "Cosmin"]) }
+ describe "fetching list of attributes" do
+ it "returns empty list when no attributes are defined" do
+ Person.attributes.should == []
+ end
+ it "returns list of attributes as symbols when they are defined" do
+ GameObject.attributes.count.should == 5
+ GameObject.attributes.each { |a| a.should be_kind_of Symbol }
+ end
+ end
+ describe "attribute methods" do
+ it "defines a reader instance method for each attribute" do
+ GameObject.attributes.each do |a|
+ subject.should respond_to a
+ end
+ end
+ it "defines a writer instance method for each attribute" do
+ GameObject.attributes.each do |a|
+ subject.should respond_to "#{a}=".to_sym
+ end
+ end
+ specify "reader returns value for attribute" do
+ subject.silver_count.should == subject['silver_count']
+ end
+ specify "writer updates value for attribute" do
+ subject.silver_count = 0
+ subject.silver_count.should == 0
+ end
+ end
+ describe "type casting" do
+ it "casts type on creation" do
+ subject['gold_count'].should == 2
+ subject.gold_count.should == 2
+ end
+ it "casts on update" do
+ subject['gold_count'] = "3"
+ subject.gold_count.should == 3
+ subject.gold_count = 4.12
+ subject['gold_count'].should == 4
+ end
+ specify "casting to String" do
+ subject.name = 1
+ subject.name.should == "1"
+ end
+ specify "casting to Float" do
+ subject.x_pos = "1.23"
+ subject['x_pos'].should == 1.23
+ end
+ specify "casting to ObjectId" do
+ id = subject.insert!
+ subject.item = id.to_s
+ subject.item.should be_kind_of BSON::ObjectId
+ end
+ end
+end
View
11 spec/spec_helper.rb
@@ -57,3 +57,14 @@ def after_remove
end
end
+class GameObject
+ include Mongomatic
+
+ attribute :silver_count
+ attribute :gold_count, :typed => Fixnum
+ attribute :name, :typed => String
+ attribute :x_pos, :typed => Float
+ attribute :item, :typed => BSON::ObjectId
+
+end
+

0 comments on commit 8f2b050

Please sign in to comment.
Something went wrong with that request. Please try again.