Started on the ExtendedDocument class with features moved to mixins.

Properties got added, they define getters, setters and aliases.
They will also be the base of the new validation system.
This commit is contained in:
Matt Aimonetti 2009-01-29 18:25:45 -08:00
parent 90f460641e
commit 750c705596
12 changed files with 533 additions and 8 deletions

View file

@ -0,0 +1,63 @@
require 'digest/md5'
module CouchRest
module Mixins
module DesignDoc
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def design_doc_id
"_design/#{design_doc_slug}"
end
def design_doc_slug
return design_doc_slug_cache if design_doc_slug_cache && design_doc_fresh
funcs = []
design_doc['views'].each do |name, view|
funcs << "#{name}/#{view['map']}#{view['reduce']}"
end
md5 = Digest::MD5.hexdigest(funcs.sort.join(''))
self.design_doc_slug_cache = "#{self.to_s}-#{md5}"
end
def default_design_doc
{
"language" => "javascript",
"views" => {
'all' => {
'map' => "function(doc) {
if (doc['couchrest-type'] == '#{self.to_s}') {
emit(null,null);
}
}"
}
}
}
end
def refresh_design_doc
did = design_doc_id
saved = database.get(did) rescue nil
if saved
design_doc['views'].each do |name, view|
saved['views'][name] = view
end
database.save_doc(saved)
self.design_doc = saved
else
design_doc['_id'] = did
design_doc.delete('_rev')
design_doc.database = database
design_doc.save
end
self.design_doc_fresh = true
end
end # module ClassMethods
end
end
end

View file

@ -0,0 +1,42 @@
module CouchRest
module Mixins
module DocumentQueries
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Load all documents that have the "couchrest-type" field equal to the
# name of the current class. Take the standard set of
# CouchRest::Database#view options.
def all(opts = {}, &block)
self.design_doc ||= Design.new(default_design_doc)
unless design_doc_fresh
refresh_design_doc
end
view :all, opts, &block
end
# Load the first document that have the "couchrest-type" field equal to
# the name of the current class.
#
# ==== Returns
# Object:: The first object instance available
# or
# Nil:: if no instances available
#
# ==== Parameters
# opts<Hash>::
# View options, see <tt>CouchRest::Database#view</tt> options for more info.
def first(opts = {})
first_instance = self.all(opts.merge!(:limit => 1))
first_instance.empty? ? nil : first_instance.first
end
end
end
end
end

View file

@ -0,0 +1,4 @@
require File.join(File.dirname(__FILE__), 'properties')
require File.join(File.dirname(__FILE__), 'document_queries')
require File.join(File.dirname(__FILE__), 'extended_views')
require File.join(File.dirname(__FILE__), 'design_doc')

View file

@ -0,0 +1,169 @@
module CouchRest
module Mixins
module ExtendedViews
def self.included(base)
base.extend(ClassMethods)
# extlib is required for the following code
base.send(:class_inheritable_accessor, :design_doc)
base.send(:class_inheritable_accessor, :design_doc_slug_cache)
base.send(:class_inheritable_accessor, :design_doc_fresh)
end
module ClassMethods
# 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 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['couchrest-type'] == 'Post' && doc.tags) {
# doc.tags.forEach(function(tag){
# emit(doc.tag, 1);
# });
# }
# }",
# :reduce =>
# "function(keys, values, rereduce) {
# return sum(values);
# }"
# end
#
# <tt>view_by :date</tt> will create a view defined by this Javascript
# function:
#
# function(doc) {
# if (doc['couchrest-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. In addition, calling with
# the <tt>:raw => true</tt> option will return the view rows
# themselves. By default <tt>Post.by_date</tt> 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 <tt>:reduce => true</tt> to return
# reduce results. The default for custom views is to query with
# <tt>:reduce => false</tt>.
#
# 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
# <tt>spec/core/model_spec.rb</tt>.
def view_by(*keys)
self.design_doc ||= Design.new(default_design_doc)
opts = keys.pop if keys.last.is_a?(Hash)
opts ||= {}
ducktype = opts.delete(:ducktype)
unless ducktype || opts[:map]
opts[:guards] ||= []
opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
end
keys.push opts
self.design_doc.view_by(*keys)
self.design_doc_fresh = false
end
# returns stored defaults if the there is a view named this in the design doc
def has_view?(view)
view = view.to_s
design_doc && design_doc['views'] && design_doc['views'][view]
end
# Dispatches to any named view.
def view name, query={}, &block
unless design_doc_fresh
refresh_design_doc
end
query[:raw] = true if query[:reduce]
raw = query.delete(:raw)
fetch_view_with_docs(name, query, raw, &block)
end
def all_design_doc_versions
database.documents :startkey => "_design/#{self.to_s}-",
:endkey => "_design/#{self.to_s}-\u9999"
end
# Deletes any non-current design docs that were created by this class.
# Running this when you're deployed version of your application is steadily
# and consistently using the latest code, is the way to clear out old design
# docs. Running it to early could mean that live code has to regenerate
# potentially large indexes.
def cleanup_design_docs!
ddocs = all_design_doc_versions
ddocs["rows"].each do |row|
if (row['id'] != design_doc_id)
database.delete_doc({
"_id" => row['id'],
"_rev" => row['value']['rev']
})
end
end
end
private
def fetch_view_with_docs name, opts, raw=false, &block
if raw
fetch_view name, opts, &block
else
begin
view = fetch_view name, opts.merge({:include_docs => true}), &block
view['rows'].collect{|r|new(r['doc'])} if view['rows']
rescue
# fallback for old versions of couchdb that don't
# have include_docs support
view = fetch_view name, opts, &block
view['rows'].collect{|r|new(database.get(r['id']))} if view['rows']
end
end
end
def fetch_view view_name, opts, &block
retryable = true
begin
design_doc.view(view_name, opts, &block)
# 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
end # module ClassMethods
end
end
end

View file

@ -0,0 +1,63 @@
module CouchRest
module Mixins
module DocumentProperties
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
# Stores the class properties
def properties
@@properties ||= []
end
# This is not a thread safe operation, if you have to set new properties at runtime
# make sure to use a mutex.
def property(name, options={})
unless properties.map{|p| p.name}.include?(name.to_s)
property = CouchRest::Property.new(name, options.delete(:type), options)
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
properties << property
end
end
protected
# defines the getter for the property
def create_property_getter(property)
meth = property.name
class_eval <<-EOS
def #{meth}
self['#{meth}']
end
EOS
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym} #{meth.to_sym}
EOS
end
end
# defines the setter for the property
def create_property_setter(property)
meth = property.name
class_eval <<-EOS
def #{meth}=(value)
self['#{meth}'] = value
end
EOS
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym}= #{meth.to_sym}=
EOS
end
end
end # module ClassMethods
end
end
end