diff --git a/lib/couchrest/core/model.rb b/lib/couchrest/core/model.rb index e3823df..3eb9eb0 100644 --- a/lib/couchrest/core/model.rb +++ b/lib/couchrest/core/model.rb @@ -1,3 +1,7 @@ +require 'rubygems' +gem 'extlib' +require 'extlib' + module CouchRest module Model class << self @@ -8,8 +12,11 @@ module CouchRest module InstanceMethods attr_accessor :doc - def initialize doc = {} - self.doc = doc + def initialize keys = {} + self.doc = {} + keys.each do |k,v| + doc[k.to_s] = v + end unless doc['_id'] && doc['_rev'] init_doc end @@ -27,7 +34,32 @@ module CouchRest doc['_rev'] end + def new_record? + !doc['_rev'] + end + def save + if new_record? + create + else + update + end + end + + protected + + def create + set_uniq_id if respond_to?(:set_uniq_id) # hack + save_doc + end + + def update + save_doc + end + + private + + def save_doc result = database.save doc if result['ok'] doc['_id'] = result['id'] @@ -36,8 +68,6 @@ module CouchRest result['ok'] end - private - def init_doc doc['type'] = self.class.to_s end @@ -53,19 +83,126 @@ module CouchRest @database || CouchRest::Model.default_database end - def uniq_id method - before_create do |model| - model.doc['_id'] = model.send(method) + def get id + doc = database.get id + new(doc) + end + + def key_accessor *keys + key_writer *keys + key_reader *keys + end + + def key_writer *keys + keys.each do |method| + key = method.to_s + define_method "#{method}=" do |value| + doc[key] = value + end end end + + def key_reader *keys + keys.each do |method| + key = method.to_s + define_method method do + doc[key] + end + end + end + + def timestamps! + before(:create) do + doc['updated_at'] = doc['created_at'] = Time.now + end + before(:update) do + doc['updated_at'] = Time.now + end + end + + def uniq_id method + define_method :set_uniq_id do + doc['_id'] ||= self.send(method) + end + end + end # module ClassMethods + + module MagicViews + def view_by *keys + type = self.to_s + doc_keys = keys.collect{|k|"doc['#{k}']"} + key_protection = doc_keys.join(' && ') + key_emit = doc_keys.length == 1 ? "#{doc_keys.first}" : "[#{doc_keys.join(', ')}]" + map_function = <<-JAVASCRIPT + function(doc) { + if (doc.type == '#{type}' && #{key_protection}) { + emit(#{key_emit}, null); + } + } + JAVASCRIPT + + method_name = "by_#{keys.join('_and_')}" + + @@design_doc ||= default_design_doc + @@design_doc['views'][method_name] = { + 'map' => map_function + } + @@design_doc_fresh = false + self.meta_class.instance_eval do + define_method method_name do + unless @@design_doc_fresh + refresh_design_doc + end + @@design_doc + end + end + end + + private + + def design_doc_id + "_design/#{self.to_s}" + end + + def default_design_doc + { + "_id" => design_doc_id, + "language" => "javascript", + "views" => {} + } + end + + + def refresh_design_doc + saved = database.get(design_doc_id) rescue nil + if saved + # merge the new views in and save if it needs to be saved + else + database.save(@@design_doc) + end + @@design_doc_fresh = true + end + + end # module MagicViews + + module Callbacks + def self.included(model) + model.class_eval <<-EOS, __FILE__, __LINE__ + include Extlib::Hook + register_instance_hooks :save #, :create, :update, :destroy + EOS + end + end # module Callbacks # bookkeeping section # load the code into the model class - def self.included(klass) - klass.extend ClassMethods - klass.send(:include, InstanceMethods) + def self.included(model) + model.send(:include, InstanceMethods) + model.extend ClassMethods + model.extend MagicViews + model.send(:include, Callbacks) end diff --git a/spec/couchrest/core/model_spec.rb b/spec/couchrest/core/model_spec.rb index 8603e9e..5695c80 100644 --- a/spec/couchrest/core/model_spec.rb +++ b/spec/couchrest/core/model_spec.rb @@ -8,6 +8,20 @@ class Article include CouchRest::Model use_database CouchRest.database!('http://localhost:5984/couchrest-model-test') uniq_id :slug + + key_accessor :title + key_reader :slug, :created_at, :updated_at + before(:create, :generate_slug_from_title) + + timestamps! + + def generate_slug_from_title + doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') + end + + key_writer :date + + view_by :date end describe CouchRest::Model do @@ -16,6 +30,9 @@ describe CouchRest::Model do @db = @cr.database(TESTDB) @db.delete! rescue nil @db = @cr.create_db(TESTDB) rescue nil + @adb = @cr.database('couchrest-model-test') + @adb.delete! rescue nil + CouchRest.database!('http://localhost:5984/couchrest-model-test') CouchRest::Model.default_database = CouchRest.database!('http://localhost:5984/couchrest-test') end @@ -27,6 +44,64 @@ describe CouchRest::Model do Article.database.info['db_name'].should == 'couchrest-model-test' end + describe "a new model" do + it "should be a new_record" do + @obj = Basic.new + @obj.should be_a_new_record + end + end + + describe "a model with key_accessors" do + it "should allow reading keys" do + @art = Article.new + @art.doc['title'] = 'My Article Title' + @art.title.should == 'My Article Title' + end + it "should allow setting keys" do + @art = Article.new + @art.title = 'My Article Title' + @art.doc['title'].should == 'My Article Title' + end + end + + describe "a model with key_writers" do + it "should allow setting keys" do + @art = Article.new + t = Time.now + @art.date = t + @art.doc['date'].should == t + end + it "should not allow reading keys" do + @art = Article.new + t = Time.now + @art.date = t + lambda{@art.date}.should raise_error + end + end + + describe "a model with key_readers" do + it "should allow reading keys" do + @art = Article.new + @art.doc['slug'] = 'my-slug' + @art.slug.should == 'my-slug' + end + it "should not allow setting keys" do + @art = Article.new + lambda{@art.slug = 'My Article Title'}.should raise_error + end + end + + describe "getting a model" do + before(:all) do + @art = Article.new(:title => 'All About Getting') + @art.save + end + it "should load and instantiate it" do + foundart = Article.get @art.id + foundart.title.should == "All About Getting" + end + end + describe "saving a model" do before(:all) do @obj = Basic.new @@ -55,13 +130,85 @@ describe CouchRest::Model do end describe "saving a model with a uniq_id configured" do - before(:all) do + before(:each) do @art = Article.new + @old = Article.database.get('this-is-the-title') rescue nil + Article.database.delete(@old) if @old end - it "should require the slug" do + + it "should require the title" do lambda{@art.save}.should raise_error - @art.slug = 'this-becomes-the-id' + @art.title = 'This is the title' @art.save.should == true end + + it "should not change the slug on update" do + @art.title = 'This is the title' + @art.save.should == true + @art.title = 'new title' + @art.save.should == true + @art.slug.should == 'this-is-the-title' + end + + it "should raise an error when the slug is taken" do + @art.title = 'This is the title' + @art.save.should == true + @art2 = Article.new(:title => 'This is the title!') + lambda{@art2.save}.should raise_error + end + + it "should set the slug" do + @art.title = 'This is the title' + @art.save.should == true + @art.slug.should == 'this-is-the-title' + end + + it "should set the id" do + @art.title = 'This is the title' + @art.save.should == true + @art.id.should == 'this-is-the-title' + end + end + + describe "a model with timestamps" do + before(:all) do + @art = Article.new(:title => "Saving this") + @art.save + end + it "should set the time on create" do + (Time.now - @art.created_at).should < 2 + foundart = Article.get @art.id + foundart.created_at.should == foundart.updated_at + end + it "should set the time on update" do + @art.save + @art.created_at.should < @art.updated_at + end + end + + describe "a model with simple views" do + before(:all) do + written_at = Time.now - 24 * 3600 * 7 + ["this and that", "also interesting", "more fun", "some junk"].each do |title| + a = Article.new(:title => title) + a.date = written_at + a.save + written_at += 24 * 3600 + end + end + + it "should create the design doc" do + Article.by_date + doc = Article.database.get("_design/Article") + doc['views']['by_date'].should_not be_nil + end + + it "should return the matching view result" do + view = Article.by_date :raw => true + # view.should == 'x' + # view['rows'].should == 4 + end + + end end \ No newline at end of file