2008-09-29 18:55:40 +02:00
module CouchRest
module Model
class << self
2008-09-30 07:56:24 +02:00
# this is the CouchRest::Database that model classes will use unless they override it with <tt>use_database</tt>
2008-09-29 18:55:40 +02:00
attr_accessor :default_database
end
# instance methods on the model classes
module InstanceMethods
attr_accessor :doc
2008-09-30 01:28:57 +02:00
def initialize keys = { }
self . doc = { }
keys . each do | k , v |
doc [ k . to_s ] = v
end
2008-09-29 18:55:40 +02:00
unless doc [ '_id' ] && doc [ '_rev' ]
init_doc
end
end
2008-09-30 07:56:24 +02:00
# returns the database used by this model's class
2008-09-29 18:55:40 +02:00
def database
self . class . database
end
2008-09-30 07:56:24 +02:00
# alias for doc['_id']
2008-09-29 18:55:40 +02:00
def id
doc [ '_id' ]
end
2008-09-30 07:56:24 +02:00
# alias for doc['_rev']
2008-09-29 18:55:40 +02:00
def rev
doc [ '_rev' ]
end
2008-09-30 07:56:24 +02:00
# returns true if the doc has never been saved
2008-09-30 01:28:57 +02:00
def new_record?
! doc [ '_rev' ]
end
2008-09-30 07:56:24 +02:00
# save the doc to the db using create or update
2008-09-29 18:55:40 +02:00
def save
2008-09-30 01:28:57 +02:00
if new_record?
create
else
update
end
end
protected
def create
2008-09-30 03:10:07 +02:00
set_unique_id if respond_to? ( :set_unique_id ) # hack
2008-09-30 01:28:57 +02:00
save_doc
end
def update
save_doc
end
private
def save_doc
2008-09-29 18:55:40 +02:00
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
# these show up as class methods on models that include CouchRest::Model
module ClassMethods
2008-09-30 07:56:24 +02:00
# override the CouchRest::Model-wide default_database
2008-09-29 18:55:40 +02:00
def use_database db
@database = db
end
2008-09-30 07:56:24 +02:00
# returns the CouchRest::Database instance that this class uses
2008-09-29 18:55:40 +02:00
def database
@database || CouchRest :: Model . default_database
end
2008-09-30 07:56:24 +02:00
# load a document from the database
2008-09-30 01:28:57 +02:00
def get id
doc = database . get id
new ( doc )
end
2008-09-30 07:56:24 +02:00
# Defines methods for reading and writing from fields in the document. Uses key_writer and key_reader internally.
2008-09-30 01:28:57 +02:00
def key_accessor * keys
key_writer * keys
key_reader * keys
end
2008-09-30 07:56:24 +02:00
# For each argument key, define a method <tt>key=</tt> that sets the corresponding field on the CouchDB document.
2008-09-30 01:28:57 +02:00
def key_writer * keys
keys . each do | method |
key = method . to_s
define_method " #{ method } = " do | value |
doc [ key ] = value
end
end
end
2008-09-30 07:56:24 +02:00
# For each argument key, define a method <tt>key</tt> that reads the corresponding field on the CouchDB document.
2008-09-30 01:28:57 +02:00
def key_reader * keys
keys . each do | method |
key = method . to_s
define_method method do
doc [ key ]
end
end
end
2008-09-30 07:56:24 +02:00
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields on the document whenever saving occurs. CouchRest uses a pretty decent time format by default. See Time#to_json
2008-09-30 01:28:57 +02:00
def timestamps!
before ( :create ) do
doc [ 'updated_at' ] = doc [ 'created_at' ] = Time . now
end
before ( :update ) do
doc [ 'updated_at' ] = Time . now
end
end
2008-09-30 07:56:24 +02:00
# Name a method that will be called before the document is first saved, which returns a string to be used for the document's <tt>_id</tt>. 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.
2008-09-30 03:10:07 +02:00
def unique_id method
define_method :set_unique_id do
2008-09-30 01:28:57 +02:00
doc [ '_id' ] || = self . send ( method )
2008-09-29 18:55:40 +02:00
end
end
2008-09-30 01:28:57 +02:00
2008-09-29 18:55:40 +02:00
end # module ClassMethods
2008-09-30 01:28:57 +02:00
module MagicViews
2008-09-30 07:56:24 +02:00
# Define a CouchDB view. The name of the view will be the concatenation of <tt>by</tt> and the keys joined by <tt>_and_</tt>
#
# ==== Example: basic view
# class Post
# view_by :date
# end
#
# This 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 <tt>Post.by_date</tt> which accepts all valid options for CouchRest::Database#view
2008-09-30 01:28:57 +02:00
def view_by * keys
2008-09-30 03:10:07 +02:00
opts = keys . pop if keys . last . is_a? ( Hash )
2008-09-30 05:18:18 +02:00
opts || = { }
2008-09-30 01:28:57 +02:00
type = self . to_s
method_name = " by_ #{ keys . join ( '_and_' ) } "
@@design_doc || = default_design_doc
2008-09-30 03:10:07 +02:00
2008-09-30 05:18:18 +02:00
if opts [ :map ]
2008-09-30 03:10:07 +02:00
view = { }
2008-09-30 05:18:18 +02:00
view [ 'map' ] = opts . delete ( :map )
if opts [ :reduce ]
view [ 'reduce' ] = opts . delete ( :reduce )
opts [ :reduce ] = false
end
2008-09-30 03:10:07 +02:00
@@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
2008-09-30 01:28:57 +02:00
@@design_doc_fresh = false
2008-09-30 02:27:41 +02:00
2008-09-30 01:28:57 +02:00
self . meta_class . instance_eval do
2008-09-30 02:46:33 +02:00
define_method method_name do | * args |
2008-09-30 05:18:18 +02:00
query = opts . merge ( args [ 0 ] || { } )
query [ :raw ] = true if query [ :reduce ]
2008-09-30 01:28:57 +02:00
unless @@design_doc_fresh
refresh_design_doc
end
2008-09-30 05:18:18 +02:00
raw = query . delete ( :raw )
2008-09-30 02:27:41 +02:00
view_name = " #{ type } / #{ method_name } "
2008-09-30 05:18:18 +02:00
view = fetch_view ( view_name , query )
2008-09-30 02:27:41 +02:00
if raw
2008-09-30 05:18:18 +02:00
view
2008-09-30 02:27:41 +02:00
else
# TODO this can be optimized once the include-docs patch is applied
view [ 'rows' ] . collect { | r | new ( database . get ( r [ 'id' ] ) ) }
end
2008-09-30 01:28:57 +02:00
end
end
end
private
2008-09-30 02:46:33 +02:00
def fetch_view view_name , opts
2008-09-30 02:27:41 +02:00
retryable = true
begin
2008-09-30 02:46:33 +02:00
database . view ( view_name , opts )
2008-09-30 02:27:41 +02:00
# 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
2008-09-30 01:28:57 +02:00
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
2008-09-30 02:27:41 +02:00
@@design_doc [ 'views' ] . each do | name , view |
saved [ 'views' ] [ name ] = view
end
database . save ( saved )
2008-09-30 01:28:57 +02:00
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
2008-09-30 02:27:41 +02:00
register_instance_hooks :save , :create , :update #, :destroy
2008-09-30 01:28:57 +02:00
EOS
end
end # module Callbacks
2008-09-29 18:55:40 +02:00
# bookkeeping section
# load the code into the model class
2008-09-30 01:28:57 +02:00
def self . included ( model )
model . send ( :include , InstanceMethods )
model . extend ClassMethods
model . extend MagicViews
model . send ( :include , Callbacks )
2008-09-29 18:55:40 +02:00
end
end # module Model
end # module CouchRest