diff --git a/lib/couchrest/core/model.rb b/lib/couchrest/core/model.rb
index 45119e7..2b390af 100644
--- a/lib/couchrest/core/model.rb
+++ b/lib/couchrest/core/model.rb
@@ -8,8 +8,7 @@ module CouchRest
#
# This is an example class using CouchRest::Model. It is taken from the spec/couchrest/core/model_spec.rb file, which may be even more up to date than this example.
#
- # class Article
- # include CouchRest::Model
+ # class Article < CouchRest::Model
# use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
# unique_id :slug
#
@@ -38,315 +37,291 @@ module CouchRest
#
# before(:create, :generate_slug_from_title)
# def generate_slug_from_title
- # doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
+ # self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
# end
# end
- module Model
+ class Model < Hash
+
+ # instantiates the hash by converting all the keys to strings.
+ def initialize keys = {}
+ super()
+ keys.each do |k,v|
+ self[k.to_s] = v
+ end
+ unless self['_id'] && self['_rev']
+ init_doc
+ end
+ end
+
class << self
# this is the CouchRest::Database that model classes will use unless they override it with use_database
attr_accessor :default_database
- end
-
- # instance methods on the model classes
- module InstanceMethods
- attr_accessor :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
- end
-
- # returns the database used by this model's class
- def database
- self.class.database
- end
-
- # alias for doc['_id']
- def id
- doc['_id']
- end
-
- # alias for doc['_rev']
- def rev
- doc['_rev']
- end
-
- # returns true if the doc has never been saved
- def new_record?
- !doc['_rev']
- end
-
- # save the doc to the db using create or update
- def save
- if new_record?
- create
- else
- update
- end
- end
-
- protected
-
- def create
- set_unique_id if respond_to?(:set_unique_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']
- doc['_rev'] = result['rev']
- end
- result['ok']
- end
-
- def init_doc
- doc['type'] = self.class.to_s
- end
- end # module InstanceMethods
-
- # Class methods for models that include CouchRest::Model
- module ClassMethods
# override the CouchRest::Model-wide default_database
- def use_database db
- @database = db
- end
-
- # returns the CouchRest::Database instance that this class uses
- def database
- @database || CouchRest::Model.default_database
- end
-
- # load a document from the database
- def get id
- doc = database.get id
- new(doc)
- end
-
- # Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
- def key_accessor *keys
- key_writer *keys
- key_reader *keys
- end
-
- # For each argument key, define a method key= that sets the corresponding field on the CouchDB document.
- def key_writer *keys
- keys.each do |method|
- key = method.to_s
- define_method "#{method}=" do |value|
- doc[key] = value
- end
- end
- end
+ def use_database db
+ @database = db
+ end
- # For each argument key, define a method key that reads the corresponding field on the CouchDB document.
- def key_reader *keys
- keys.each do |method|
- key = method.to_s
- define_method method do
- doc[key]
- end
- end
- end
-
- # Automatically set updated_at and created_at fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
- def timestamps!
- before(:create) do
- doc['updated_at'] = doc['created_at'] = Time.now
- end
- before(:update) do
- doc['updated_at'] = Time.now
- end
- end
-
- # Name a method that will be called before the document is first saved, which returns a string to be used for the document's _id. Because CouchDB enforces a constraint that each id must be unique, this can be used to enforce eg: uniq usernames. Note that this id must be globally unique across all document types which share a database, so if you'd like to scope uniqueness to this class, you should use the class name as part of the unique id.
- def unique_id method
- define_method :set_unique_id do
- doc['_id'] ||= self.send(method)
- end
- end
-
- end # module ClassMethods
+ # returns the CouchRest::Database instance that this class uses
+ def database
+ @database || CouchRest::Model.default_database
+ end
- module MagicViews
-
- # Define a CouchDB view. The name of the view will be the concatenation of by and the keys joined by _and_
- #
- # ==== Example views:
- #
- # class Post
- # # view with default options
- # # query with Post.by_date
- # view_by :date, :descending => true
- #
- # # view with compound sort-keys
- # # query with Post.by_user_id_and_date
- # view_by :user_id, :date
- #
- # # view with custom map/reduce functions
- # # query with Post.by_tags :reduce => true
- # view_by :tags,
- # :map =>
- # "function(doc) {
- # if (doc.type == 'Post' && doc.tags) {
- # doc.tags.forEach(function(tag){
- # emit(doc.tag, 1);
- # });
- # }
- # }",
- # :reduce =>
- # "function(keys, values, rereduce) {
- # return sum(values);
- # }"
- # end
- #
- # view_by :date will create a view defined by this Javascript function:
- #
- # function(doc) {
- # if (doc.type == 'Post' && doc.date) {
- # emit(doc.date, null);
- # }
- # }
- #
- # It can be queried by calling Post.by_date which accepts all valid options for CouchRest::Database#view. In addition, calling with the :raw => true option will return the view rows themselves. By default Post.by_date will return the documents included in the generated view.
- #
- # CouchRest::Database#view options can be applied at view definition time as defaults, and they will be curried and used at view query time. Or they can be overridden at query time.
- #
- # Custom views can be queried with :reduce => true to return reduce results. The default for custom views is to query with :reduce => false.
- #
- # Views are generated (on a per-model basis) lazily on first-access. This means that if you are deploying changes to a view, the views for that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
- #
- # To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at spec/core/model.rb.
- def view_by *keys
- opts = keys.pop if keys.last.is_a?(Hash)
- opts ||= {}
- type = self.to_s
+ # load a document from the database
+ def get id
+ doc = database.get id
+ new(doc)
+ end
- method_name = "by_#{keys.join('_and_')}"
- @@design_doc ||= default_design_doc
-
- if opts[:map]
- view = {}
- view['map'] = opts.delete(:map)
- if opts[:reduce]
- view['reduce'] = opts.delete(:reduce)
- opts[:reduce] = false
- end
- @@design_doc['views'][method_name] = view
- else
- 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
- @@design_doc['views'][method_name] = {
- 'map' => map_function
- }
- end
-
- @@design_doc_fresh = false
-
- self.meta_class.instance_eval do
- define_method method_name do |*args|
- query = opts.merge(args[0] || {})
- query[:raw] = true if query[:reduce]
- unless @@design_doc_fresh
- refresh_design_doc
- end
- raw = query.delete(:raw)
- view_name = "#{type}/#{method_name}"
+ # Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
+ def key_accessor *keys
+ key_writer *keys
+ key_reader *keys
+ end
+
+ # For each argument key, define a method key= that sets the corresponding field on the CouchDB document.
+ def key_writer *keys
+ keys.each do |method|
+ key = method.to_s
+ define_method "#{method}=" do |value|
+ self[key] = value
+ end
+ end
+ end
+
+ # For each argument key, define a method key that reads the corresponding field on the CouchDB document.
+ def key_reader *keys
+ keys.each do |method|
+ key = method.to_s
+ define_method method do
+ self[key]
+ end
+ end
+ end
+
+ # Automatically set updated_at and created_at fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
+ def timestamps!
+ before(:create) do
+ self['updated_at'] = self['created_at'] = Time.now
+ end
+ before(:update) do
+ self['updated_at'] = Time.now
+ end
+ end
+
+ # Name a method that will be called before the document is first saved, which returns a string to be used for the document's _id. Because CouchDB enforces a constraint that each id must be unique, this can be used to enforce eg: uniq usernames. Note that this id must be globally unique across all document types which share a database, so if you'd like to scope uniqueness to this class, you should use the class name as part of the unique id.
+ def unique_id method
+ define_method :set_unique_id do
+ self['_id'] ||= self.send(method)
+ end
+ end
+
+ # Define a CouchDB view. The name of the view will be the concatenation of by and the keys joined by _and_
+ #
+ # ==== Example views:
+ #
+ # class Post
+ # # view with default options
+ # # query with Post.by_date
+ # view_by :date, :descending => true
+ #
+ # # view with compound sort-keys
+ # # query with Post.by_user_id_and_date
+ # view_by :user_id, :date
+ #
+ # # view with custom map/reduce functions
+ # # query with Post.by_tags :reduce => true
+ # view_by :tags,
+ # :map =>
+ # "function(doc) {
+ # if (doc.type == 'Post' && doc.tags) {
+ # doc.tags.forEach(function(tag){
+ # emit(doc.tag, 1);
+ # });
+ # }
+ # }",
+ # :reduce =>
+ # "function(keys, values, rereduce) {
+ # return sum(values);
+ # }"
+ # end
+ #
+ # view_by :date will create a view defined by this Javascript function:
+ #
+ # function(doc) {
+ # if (doc.type == 'Post' && doc.date) {
+ # emit(doc.date, null);
+ # }
+ # }
+ #
+ # It can be queried by calling Post.by_date which accepts all valid options for CouchRest::Database#view. In addition, calling with the :raw => true option will return the view rows themselves. By default Post.by_date will return the documents included in the generated view.
+ #
+ # CouchRest::Database#view options can be applied at view definition time as defaults, and they will be curried and used at view query time. Or they can be overridden at query time.
+ #
+ # Custom views can be queried with :reduce => true to return reduce results. The default for custom views is to query with :reduce => false.
+ #
+ # Views are generated (on a per-model basis) lazily on first-access. This means that if you are deploying changes to a view, the views for that model won't be available until generation is complete. This can take some time with large databases. Strategies are in the works.
+ #
+ # To understand the capabilities of this view system more compeletly, it is recommended that you read the RSpec file at spec/core/model.rb.
+ def view_by *keys
+ opts = keys.pop if keys.last.is_a?(Hash)
+ opts ||= {}
+ type = self.to_s
+
+ method_name = "by_#{keys.join('_and_')}"
+ @@design_doc ||= default_design_doc
+
+ if opts[:map]
+ view = {}
+ view['map'] = opts.delete(:map)
+ if opts[:reduce]
+ view['reduce'] = opts.delete(:reduce)
+ opts[:reduce] = false
+ end
+ @@design_doc['views'][method_name] = view
+ else
+ 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
+ @@design_doc['views'][method_name] = {
+ 'map' => map_function
+ }
+ end
+
+ @@design_doc_fresh = false
+
+ self.meta_class.instance_eval do
+ define_method method_name do |*args|
+ query = opts.merge(args[0] || {})
+ query[:raw] = true if query[:reduce]
+ unless @@design_doc_fresh
+ refresh_design_doc
+ end
+ raw = query.delete(:raw)
+ view_name = "#{type}/#{method_name}"
+
+ view = fetch_view(view_name, query)
+ if raw
+ view
+ else
+ # TODO this can be optimized once the include-docs patch is applied
+ view['rows'].collect{|r|new(database.get(r['id']))}
+ end
+ end
+ end
+ end
+
+ private
+
+ def fetch_view view_name, opts
+ retryable = true
+ begin
+ database.view(view_name, opts)
+ # the design doc could have been deleted by a rouge process
+ rescue RestClient::ResourceNotFound => e
+ if retryable
+ refresh_design_doc
+ retryable = false
+ retry
+ else
+ raise e
+ end
+ end
+ end
+
+ 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
+ @@design_doc['views'].each do |name, view|
+ saved['views'][name] = view
+ end
+ database.save(saved)
+ else
+ database.save(@@design_doc)
+ end
+ @@design_doc_fresh = true
+ end
+
+ end # class << self
+
- view = fetch_view(view_name, query)
- if raw
- view
- else
- # TODO this can be optimized once the include-docs patch is applied
- view['rows'].collect{|r|new(database.get(r['id']))}
- end
- end
- end
- end
-
- private
-
- def fetch_view view_name, opts
- retryable = true
- begin
- database.view(view_name, opts)
- # the design doc could have been deleted by a rouge process
- rescue RestClient::ResourceNotFound => e
- if retryable
- refresh_design_doc
- retryable = false
- retry
- else
- raise e
- end
- end
- end
-
- 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
- @@design_doc['views'].each do |name, view|
- saved['views'][name] = view
- end
- database.save(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(model)
- model.send(:include, InstanceMethods)
- model.extend ClassMethods
- model.extend MagicViews
- model.send(:include, Callbacks)
+ # returns the database used by this model's class
+ def database
+ self.class.database
end
- end # module Model
+ # alias for self['_id']
+ def id
+ self['_id']
+ end
+
+ # alias for self['_rev']
+ def rev
+ self['_rev']
+ end
+
+ # returns true if the self has never been saved
+ def new_record?
+ !rev
+ end
+
+ # save the doc to the db using create or update
+ def save
+ if new_record?
+ create
+ else
+ update
+ end
+ end
+
+ protected
+
+ def create
+ set_unique_id if respond_to?(:set_unique_id) # hack
+ save_doc
+ end
+
+ def update
+ save_doc
+ end
+
+ private
+
+ def save_doc
+ result = database.save self
+ if result['ok']
+ self['_id'] = result['id']
+ self['_rev'] = result['rev']
+ end
+ result['ok']
+ end
+
+ def init_doc
+ self['type'] = self.class.to_s
+ end
+
+ include ::Extlib::Hook
+ register_instance_hooks :save, :create, :update #, :destroy
+
+ end # class Model
end # module CouchRest
\ No newline at end of file
diff --git a/spec/couchrest/core/model_spec.rb b/spec/couchrest/core/model_spec.rb
index fc490b2..153f933 100644
--- a/spec/couchrest/core/model_spec.rb
+++ b/spec/couchrest/core/model_spec.rb
@@ -1,11 +1,10 @@
require File.dirname(__FILE__) + '/../../spec_helper'
-class Basic
- include CouchRest::Model
+class Basic < CouchRest::Model
+
end
-class Article
- include CouchRest::Model
+class Article < CouchRest::Model
use_database CouchRest.database!('http://localhost:5984/couchrest-model-test')
unique_id :slug
@@ -34,7 +33,7 @@ class Article
before(:create, :generate_slug_from_title)
def generate_slug_from_title
- doc['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
+ self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'')
end
end
@@ -61,6 +60,7 @@ describe CouchRest::Model do
describe "a new model" do
it "should be a new_record" do
@obj = Basic.new
+ @obj.rev.should be_nil
@obj.should be_a_new_record
end
end
@@ -68,13 +68,13 @@ describe CouchRest::Model do
describe "a model with key_accessors" do
it "should allow reading keys" do
@art = Article.new
- @art.doc['title'] = 'My Article Title'
+ @art['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'
+ @art['title'].should == 'My Article Title'
end
end
@@ -83,7 +83,7 @@ describe CouchRest::Model do
@art = Article.new
t = Time.now
@art.date = t
- @art.doc['date'].should == t
+ @art['date'].should == t
end
it "should not allow reading keys" do
@art = Article.new
@@ -96,7 +96,7 @@ describe CouchRest::Model do
describe "a model with key_readers" do
it "should allow reading keys" do
@art = Article.new
- @art.doc['slug'] = 'my-slug'
+ @art['slug'] = 'my-slug'
@art.slug.should == 'my-slug'
end
it "should not allow setting keys" do
@@ -129,7 +129,7 @@ describe CouchRest::Model do
it "should be set for resaving" do
rev = @obj.rev
- @obj.doc['another-key'] = "some value"
+ @obj['another-key'] = "some value"
@obj.save
@obj.rev.should_not == rev
end
@@ -139,7 +139,7 @@ describe CouchRest::Model do
end
it "should set the type" do
- @obj.doc['type'].should == 'Basic'
+ @obj['type'].should == 'Basic'
end
end
@@ -254,7 +254,7 @@ describe CouchRest::Model do
end
it "should sort correctly" do
articles = Article.by_user_id_and_date
- articles.collect{|a|a.doc['user_id']}.should == ['aaron', 'aaron', 'quentin', 'quentin']
+ articles.collect{|a|a['user_id']}.should == ['aaron', 'aaron', 'quentin', 'quentin']
articles[1].title.should == 'not junk'
end
it "should be queryable with couchrest options" do