Renaming to CouchRest Model
Refactored basic directory structure. Moved to ActiveSupport for Validations and Callbacks. Cleaned up older code, and removed support for text property types.
This commit is contained in:
parent
9f1eea8d32
commit
c280b3a29b
70 changed files with 1725 additions and 3733 deletions
207
lib/couchrest/model/associations.rb
Normal file
207
lib/couchrest/model/associations.rb
Normal file
|
@ -0,0 +1,207 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Associations
|
||||
|
||||
# Basic support for relationships between ExtendedDocuments
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Define an association that this object belongs to.
|
||||
#
|
||||
def belongs_to(attrib, *options)
|
||||
opts = {
|
||||
:foreign_key => attrib.to_s + '_id',
|
||||
:class_name => attrib.to_s.camelcase,
|
||||
:proxy => nil
|
||||
}
|
||||
case options.first
|
||||
when Hash
|
||||
opts.merge!(options.first)
|
||||
end
|
||||
|
||||
begin
|
||||
opts[:class] = opts[:class_name].constantize
|
||||
rescue
|
||||
raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
||||
end
|
||||
|
||||
prop = property(opts[:foreign_key], opts)
|
||||
|
||||
create_belongs_to_getter(attrib, prop, opts)
|
||||
create_belongs_to_setter(attrib, prop, opts)
|
||||
|
||||
prop
|
||||
end
|
||||
|
||||
# Provide access to a collection of objects where the associated
|
||||
# property contains a list of the collection item ids.
|
||||
#
|
||||
# The following:
|
||||
#
|
||||
# collection_of :groups
|
||||
#
|
||||
# creates a pseudo property called "groups" which allows access
|
||||
# to a CollectionOfProxy object. Adding, replacing or removing entries in this
|
||||
# proxy will cause the matching property array, in this case "group_ids", to
|
||||
# be kept in sync.
|
||||
#
|
||||
# Any manual changes made to the collection ids property (group_ids), unless replaced, will require
|
||||
# a reload of the CollectionOfProxy for the two sets of data to be in sync:
|
||||
#
|
||||
# group_ids = ['123']
|
||||
# groups == [Group.get('123')]
|
||||
# group_ids << '321'
|
||||
# groups == [Group.get('123')]
|
||||
# groups(true) == [Group.get('123'), Group.get('321')]
|
||||
#
|
||||
# Of course, saving the parent record will store the collection ids as they are
|
||||
# found.
|
||||
#
|
||||
# The CollectionOfProxy supports the following array functions, anything else will cause
|
||||
# a mismatch between the collection objects and collection ids:
|
||||
#
|
||||
# groups << obj
|
||||
# groups.push obj
|
||||
# groups.unshift obj
|
||||
# groups[0] = obj
|
||||
# groups.pop == obj
|
||||
# groups.shift == obj
|
||||
#
|
||||
# Addtional options match those of the the belongs_to method.
|
||||
#
|
||||
# NOTE: This method is *not* recommended for large collections or collections that change
|
||||
# frequently! Use with prudence.
|
||||
#
|
||||
def collection_of(attrib, *options)
|
||||
opts = {
|
||||
:foreign_key => attrib.to_s.singularize + '_ids',
|
||||
:class_name => attrib.to_s.singularize.camelcase,
|
||||
:proxy => nil
|
||||
}
|
||||
case options.first
|
||||
when Hash
|
||||
opts.merge!(options.first)
|
||||
end
|
||||
begin
|
||||
opts[:class] = opts[:class_name].constantize
|
||||
rescue
|
||||
raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
||||
end
|
||||
opts[:readonly] = true
|
||||
|
||||
prop = property(opts[:foreign_key], [], opts)
|
||||
|
||||
create_collection_of_property_setter(attrib, prop, opts)
|
||||
create_collection_of_getter(attrib, prop, opts)
|
||||
create_collection_of_setter(attrib, prop, opts)
|
||||
|
||||
prop
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def create_belongs_to_getter(attrib, property, options)
|
||||
base = options[:proxy] || options[:class_name]
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}
|
||||
@#{attrib} ||= #{base}.get(self.#{options[:foreign_key]})
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
def create_belongs_to_setter(attrib, property, options)
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}=(value)
|
||||
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
|
||||
@#{attrib} = value
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
### collection_of support methods
|
||||
|
||||
def create_collection_of_property_setter(attrib, property, options)
|
||||
# ensure CollectionOfProxy is nil, ready to be reloaded on request
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{options[:foreign_key]}=(value)
|
||||
@#{attrib} = nil
|
||||
write_attribute("#{options[:foreign_key]}", value)
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
def create_collection_of_getter(attrib, property, options)
|
||||
base = options[:proxy] || options[:class_name]
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}(reload = false)
|
||||
return @#{attrib} unless @#{attrib}.nil? or reload
|
||||
ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
|
||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
def create_collection_of_setter(attrib, property, options)
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}=(value)
|
||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}')
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Special proxy for a collection of items so that adding and removing
|
||||
# to the list automatically updates the associated property.
|
||||
class CollectionOfProxy < Array
|
||||
attr_accessor :property
|
||||
attr_accessor :casted_by
|
||||
|
||||
def initialize(array, casted_by, property)
|
||||
self.property = property
|
||||
self.casted_by = casted_by
|
||||
(array ||= []).compact!
|
||||
casted_by[property.to_s] = [] # replace the original array!
|
||||
array.compact.each do |obj|
|
||||
casted_by[property.to_s] << obj.id
|
||||
end
|
||||
super(array)
|
||||
end
|
||||
def << obj
|
||||
casted_by[property.to_s] << obj.id
|
||||
super(obj)
|
||||
end
|
||||
def push(obj)
|
||||
casted_by[property.to_s].push obj.id
|
||||
super(obj)
|
||||
end
|
||||
def unshift(obj)
|
||||
casted_by[property.to_s].unshift obj.id
|
||||
super(obj)
|
||||
end
|
||||
|
||||
def []= index, obj
|
||||
casted_by[property.to_s][index] = obj.id
|
||||
super(index, obj)
|
||||
end
|
||||
|
||||
def pop
|
||||
casted_by[property.to_s].pop
|
||||
super
|
||||
end
|
||||
def shift
|
||||
casted_by[property.to_s].shift
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
74
lib/couchrest/model/attribute_protection.rb
Normal file
74
lib/couchrest/model/attribute_protection.rb
Normal file
|
@ -0,0 +1,74 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module AttributeProtection
|
||||
# Attribute protection from mass assignment to CouchRest properties
|
||||
#
|
||||
# Protected methods will be removed from
|
||||
# * new
|
||||
# * update_attributes
|
||||
# * upate_attributes_without_saving
|
||||
# * attributes=
|
||||
#
|
||||
# There are two modes of protection
|
||||
# 1) Declare accessible poperties, assume all the rest are protected
|
||||
# property :name, :accessible => true
|
||||
# property :admin # this will be automatically protected
|
||||
#
|
||||
# 2) Declare protected properties, assume all the rest are accessible
|
||||
# property :name # this will not be protected
|
||||
# property :admin, :protected => true
|
||||
#
|
||||
# Note: you cannot set both flags in a single class
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def accessible_properties
|
||||
properties.select { |prop| prop.options[:accessible] }
|
||||
end
|
||||
|
||||
def protected_properties
|
||||
properties.select { |prop| prop.options[:protected] }
|
||||
end
|
||||
end
|
||||
|
||||
def accessible_properties
|
||||
self.class.accessible_properties
|
||||
end
|
||||
|
||||
def protected_properties
|
||||
self.class.protected_properties
|
||||
end
|
||||
|
||||
def remove_protected_attributes(attributes)
|
||||
protected_names = properties_to_remove_from_mass_assignment.map { |prop| prop.name }
|
||||
return attributes if protected_names.empty?
|
||||
|
||||
attributes.reject! do |property_name, property_value|
|
||||
protected_names.include?(property_name.to_s)
|
||||
end
|
||||
|
||||
attributes || {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def properties_to_remove_from_mass_assignment
|
||||
has_protected = !protected_properties.empty?
|
||||
has_accessible = !accessible_properties.empty?
|
||||
|
||||
if !has_protected && !has_accessible
|
||||
[]
|
||||
elsif has_protected && !has_accessible
|
||||
protected_properties
|
||||
elsif has_accessible && !has_protected
|
||||
properties.reject { |prop| prop.options[:accessible] }
|
||||
else
|
||||
raise "Set either :accessible or :protected for #{self.class}, but not both"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
75
lib/couchrest/model/attributes.rb
Normal file
75
lib/couchrest/model/attributes.rb
Normal file
|
@ -0,0 +1,75 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Attributes
|
||||
|
||||
## Support for handling attributes
|
||||
#
|
||||
# This would be better in the properties file, but due to scoping issues
|
||||
# this is not yet possible.
|
||||
#
|
||||
|
||||
def prepare_all_attributes(doc = {}, options = {})
|
||||
apply_all_property_defaults
|
||||
if options[:directly_set_attributes]
|
||||
directly_set_read_only_attributes(doc)
|
||||
else
|
||||
remove_protected_attributes(doc)
|
||||
end
|
||||
directly_set_attributes(doc) unless doc.nil?
|
||||
end
|
||||
|
||||
# Takes a hash as argument, and applies the values by using writer methods
|
||||
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
||||
# missing. In case of error, no attributes are changed.
|
||||
def update_attributes_without_saving(hash)
|
||||
# Remove any protected and update all the rest. Any attributes
|
||||
# which do not have a property will simply be ignored.
|
||||
attrs = remove_protected_attributes(hash)
|
||||
directly_set_attributes(attrs)
|
||||
end
|
||||
alias :attributes= :update_attributes_without_saving
|
||||
|
||||
# Takes a hash as argument, and applies the values by using writer methods
|
||||
# for each key. Raises a NoMethodError if the corresponding methods are
|
||||
# missing. In case of error, no attributes are changed.
|
||||
def update_attributes(hash)
|
||||
update_attributes_without_saving hash
|
||||
save
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def directly_set_attributes(hash)
|
||||
hash.each do |attribute_name, attribute_value|
|
||||
if self.respond_to?("#{attribute_name}=")
|
||||
self.send("#{attribute_name}=", hash.delete(attribute_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def directly_set_read_only_attributes(hash)
|
||||
property_list = self.properties.map{|p| p.name}
|
||||
hash.each do |attribute_name, attribute_value|
|
||||
next if self.respond_to?("#{attribute_name}=")
|
||||
if property_list.include?(attribute_name)
|
||||
write_attribute(attribute_name, hash.delete(attribute_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_attributes(hash)
|
||||
attrs = remove_protected_attributes(hash)
|
||||
directly_set_attributes(attrs)
|
||||
end
|
||||
|
||||
def check_properties_exist(attrs)
|
||||
property_list = self.properties.map{|p| p.name}
|
||||
attrs.each do |attribute_name, attribute_value|
|
||||
raise NoMethodError, "Property #{attribute_name} not created" unless respond_to?("#{attribute_name}=") or property_list.include?(attribute_name)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
93
lib/couchrest/model/base.rb
Normal file
93
lib/couchrest/model/base.rb
Normal file
|
@ -0,0 +1,93 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
class Base < Document
|
||||
|
||||
extend ActiveModel::Naming
|
||||
|
||||
include CouchRest::Model::Persistence
|
||||
include CouchRest::Model::Callbacks
|
||||
include CouchRest::Model::DocumentQueries
|
||||
include CouchRest::Model::Views
|
||||
include CouchRest::Model::DesignDoc
|
||||
include CouchRest::Model::ExtendedAttachments
|
||||
include CouchRest::Model::ClassProxy
|
||||
include CouchRest::Model::Collection
|
||||
include CouchRest::Model::AttributeProtection
|
||||
include CouchRest::Model::Attributes
|
||||
include CouchRest::Model::Associations
|
||||
include CouchRest::Model::Validations
|
||||
|
||||
def self.subclasses
|
||||
@subclasses ||= []
|
||||
end
|
||||
|
||||
def self.inherited(subklass)
|
||||
super
|
||||
subklass.send(:include, CouchRest::Model::Properties)
|
||||
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def self.inherited(subklass)
|
||||
super
|
||||
subklass.properties = self.properties.dup
|
||||
# This is nasty:
|
||||
subklass._validators = self._validators.dup
|
||||
end
|
||||
EOS
|
||||
subclasses << subklass
|
||||
end
|
||||
|
||||
# Accessors
|
||||
attr_accessor :casted_by
|
||||
|
||||
|
||||
# Instantiate a new ExtendedDocument by preparing all properties
|
||||
# using the provided document hash.
|
||||
#
|
||||
# Options supported:
|
||||
#
|
||||
# * :directly_set_attributes: true when data comes directly from database
|
||||
#
|
||||
def initialize(doc = {}, options = {})
|
||||
prepare_all_attributes(doc, options)
|
||||
super(doc)
|
||||
unless self['_id'] && self['_rev']
|
||||
self['couchrest-type'] = self.class.to_s
|
||||
end
|
||||
after_initialize if respond_to?(:after_initialize)
|
||||
end
|
||||
|
||||
|
||||
# Temp solution to make the view_by methods available
|
||||
def self.method_missing(m, *args, &block)
|
||||
if has_view?(m)
|
||||
query = args.shift || {}
|
||||
return view(m, query, *args, &block)
|
||||
elsif m.to_s =~ /^find_(by_.+)/
|
||||
view_name = $1
|
||||
if has_view?(view_name)
|
||||
return first_from_view(view_name, *args)
|
||||
end
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
### instance methods
|
||||
|
||||
# Gets a reference to the actual document in the DB
|
||||
# Calls up to the next document if there is one,
|
||||
# Otherwise we're at the top and we return self
|
||||
def base_doc
|
||||
return self if base_doc?
|
||||
@casted_by.base_doc
|
||||
end
|
||||
|
||||
# Checks if we're the top document
|
||||
def base_doc?
|
||||
!@casted_by
|
||||
end
|
||||
|
||||
# for compatibility with old-school frameworks
|
||||
alias :new_record? :new?
|
||||
alias :new_document? :new?
|
||||
end
|
||||
end
|
||||
end
|
27
lib/couchrest/model/callbacks.rb
Normal file
27
lib/couchrest/model/callbacks.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# encoding: utf-8
|
||||
|
||||
module CouchRest #:nodoc:
|
||||
module Model #:nodoc:
|
||||
|
||||
module Callbacks
|
||||
extend ActiveSupport::Concern
|
||||
included do
|
||||
extend ActiveModel::Callbacks
|
||||
|
||||
define_model_callbacks \
|
||||
:create,
|
||||
:destroy,
|
||||
:save,
|
||||
:update,
|
||||
:validate
|
||||
|
||||
end
|
||||
|
||||
def valid?(*) #nodoc
|
||||
_run_validation_callbacks { super }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
39
lib/couchrest/model/casted_array.rb
Normal file
39
lib/couchrest/model/casted_array.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
#
|
||||
# Wrapper around Array so that the casted_by attribute is set in all
|
||||
# elements of the array.
|
||||
#
|
||||
|
||||
module CouchRest::Model
|
||||
class CastedArray < Array
|
||||
attr_accessor :casted_by
|
||||
attr_accessor :property
|
||||
|
||||
def initialize(array, property)
|
||||
self.property = property
|
||||
super(array)
|
||||
end
|
||||
|
||||
def << obj
|
||||
super(instantiate_and_cast(obj))
|
||||
end
|
||||
|
||||
def push(obj)
|
||||
super(instantiate_and_cast(obj))
|
||||
end
|
||||
|
||||
def []= index, obj
|
||||
super(index, instantiate_and_cast(obj))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def instantiate_and_cast(obj)
|
||||
if self.casted_by && self.property && obj.class != self.property.type_class
|
||||
self.property.cast_value(self.casted_by, obj)
|
||||
else
|
||||
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
|
||||
obj
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
56
lib/couchrest/model/casted_model.rb
Normal file
56
lib/couchrest/model/casted_model.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
module CouchRest::Model
|
||||
module CastedModel
|
||||
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include CouchRest::Model::AttributeProtection
|
||||
include CouchRest::Model::Attributes
|
||||
include CouchRest::Model::Callbacks
|
||||
include CouchRest::Model::Properties
|
||||
include CouchRest::Model::Associations
|
||||
include CouchRest::Model::Validations
|
||||
attr_accessor :casted_by
|
||||
end
|
||||
|
||||
def initialize(keys = {})
|
||||
raise StandardError unless self.is_a? Hash
|
||||
prepare_all_attributes(keys)
|
||||
super()
|
||||
end
|
||||
|
||||
def []= key, value
|
||||
super(key.to_s, value)
|
||||
end
|
||||
|
||||
def [] key
|
||||
super(key.to_s)
|
||||
end
|
||||
|
||||
# Gets a reference to the top level extended
|
||||
# document that a model is saved inside of
|
||||
def base_doc
|
||||
return nil unless @casted_by
|
||||
@casted_by.base_doc
|
||||
end
|
||||
|
||||
# False if the casted model has already
|
||||
# been saved in the containing document
|
||||
def new?
|
||||
@casted_by.nil? ? true : @casted_by.new?
|
||||
end
|
||||
alias :new_record? :new?
|
||||
|
||||
# Sets the attributes from a hash
|
||||
def update_attributes_without_saving(hash)
|
||||
hash.each do |k, v|
|
||||
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
|
||||
end
|
||||
hash.each do |k, v|
|
||||
self.send("#{k}=",v)
|
||||
end
|
||||
end
|
||||
alias :attributes= :update_attributes_without_saving
|
||||
|
||||
end
|
||||
end
|
122
lib/couchrest/model/class_proxy.rb
Normal file
122
lib/couchrest/model/class_proxy.rb
Normal file
|
@ -0,0 +1,122 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module ClassProxy
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Return a proxy object which represents a model class on a
|
||||
# chosen database instance. This allows you to DRY operations
|
||||
# where a database is chosen dynamically.
|
||||
#
|
||||
# ==== Example:
|
||||
#
|
||||
# db = CouchRest::Database.new(...)
|
||||
# articles = Article.on(db)
|
||||
#
|
||||
# articles.all { ... }
|
||||
# articles.by_title { ... }
|
||||
#
|
||||
# u = articles.get("someid")
|
||||
#
|
||||
# u = articles.new(:title => "I like plankton")
|
||||
# u.save # saved on the correct database
|
||||
|
||||
def on(database)
|
||||
Proxy.new(self, database)
|
||||
end
|
||||
end
|
||||
|
||||
class Proxy #:nodoc:
|
||||
def initialize(klass, database)
|
||||
@klass = klass
|
||||
@database = database
|
||||
end
|
||||
|
||||
# Base
|
||||
|
||||
def new(*args)
|
||||
doc = @klass.new(*args)
|
||||
doc.database = @database
|
||||
doc
|
||||
end
|
||||
|
||||
def method_missing(m, *args, &block)
|
||||
if has_view?(m)
|
||||
query = args.shift || {}
|
||||
return view(m, query, *args, &block)
|
||||
elsif m.to_s =~ /^find_(by_.+)/
|
||||
view_name = $1
|
||||
if has_view?(view_name)
|
||||
return first_from_view(view_name, *args)
|
||||
end
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# DocumentQueries
|
||||
|
||||
def all(opts = {}, &block)
|
||||
docs = @klass.all({:database => @database}.merge(opts), &block)
|
||||
docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
|
||||
docs
|
||||
end
|
||||
|
||||
def count(opts = {}, &block)
|
||||
@klass.all({:database => @database, :raw => true, :limit => 0}.merge(opts), &block)['total_rows']
|
||||
end
|
||||
|
||||
def first(opts = {})
|
||||
doc = @klass.first({:database => @database}.merge(opts))
|
||||
doc.database = @database if doc && doc.respond_to?(:database)
|
||||
doc
|
||||
end
|
||||
|
||||
def get(id)
|
||||
doc = @klass.get(id, @database)
|
||||
doc.database = @database if doc && doc.respond_to?(:database)
|
||||
doc
|
||||
end
|
||||
alias :find :get
|
||||
|
||||
# Views
|
||||
|
||||
def has_view?(view)
|
||||
@klass.has_view?(view)
|
||||
end
|
||||
|
||||
def view(name, query={}, &block)
|
||||
docs = @klass.view(name, {:database => @database}.merge(query), &block)
|
||||
docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
|
||||
docs
|
||||
end
|
||||
|
||||
def first_from_view(name, *args)
|
||||
# add to first hash available, or add to end
|
||||
(args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
|
||||
doc = @klass.first_from_view(name, *args)
|
||||
doc.database = @database if doc && doc.respond_to?(:database)
|
||||
doc
|
||||
end
|
||||
|
||||
# DesignDoc
|
||||
|
||||
def design_doc
|
||||
@klass.design_doc
|
||||
end
|
||||
|
||||
def refresh_design_doc
|
||||
@klass.refresh_design_doc(@database)
|
||||
end
|
||||
|
||||
def save_design_doc
|
||||
@klass.save_design_doc(@database)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
260
lib/couchrest/model/collection.rb
Normal file
260
lib/couchrest/model/collection.rb
Normal file
|
@ -0,0 +1,260 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Collection
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Creates a new class method, find_all_<collection_name>, that will
|
||||
# execute the view specified with the design_doc and view_name
|
||||
# parameters, along with the specified view_options. This method will
|
||||
# return the results of the view as an Array of objects which are
|
||||
# instances of the class.
|
||||
#
|
||||
# This method is handy for objects that do not use the view_by method
|
||||
# to declare their views.
|
||||
def provides_collection(collection_name, design_doc, view_name, view_options)
|
||||
class_eval <<-END, __FILE__, __LINE__ + 1
|
||||
def self.find_all_#{collection_name}(options = {})
|
||||
view_options = #{view_options.inspect} || {}
|
||||
CollectionProxy.new(options[:database] || database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
|
||||
end
|
||||
END
|
||||
end
|
||||
|
||||
# Fetch a group of objects from CouchDB. Options can include:
|
||||
# :page - Specifies the page to load (starting at 1)
|
||||
# :per_page - Specifies the number of objects to load per page
|
||||
#
|
||||
# Defaults are used if these options are not specified.
|
||||
def paginate(options)
|
||||
proxy = create_collection_proxy(options)
|
||||
proxy.paginate(options)
|
||||
end
|
||||
|
||||
# Iterate over the objects in a collection, fetching them from CouchDB
|
||||
# in groups. Options can include:
|
||||
# :page - Specifies the page to load
|
||||
# :per_page - Specifies the number of objects to load per page
|
||||
#
|
||||
# Defaults are used if these options are not specified.
|
||||
def paginated_each(options, &block)
|
||||
search = options.delete(:search)
|
||||
unless search == true
|
||||
proxy = create_collection_proxy(options)
|
||||
else
|
||||
proxy = create_search_collection_proxy(options)
|
||||
end
|
||||
proxy.paginated_each(options, &block)
|
||||
end
|
||||
|
||||
# Create a CollectionProxy for the specified view and options.
|
||||
# CollectionProxy behaves just like an Array, but offers support for
|
||||
# pagination.
|
||||
def collection_proxy_for(design_doc, view_name, view_options = {})
|
||||
options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
|
||||
create_collection_proxy(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_collection_proxy(options)
|
||||
design_doc, view_name, view_options = parse_view_options(options)
|
||||
CollectionProxy.new(options[:database] || database, design_doc, view_name, view_options, self)
|
||||
end
|
||||
|
||||
def create_search_collection_proxy(options)
|
||||
design_doc, search_name, search_options = parse_search_options(options)
|
||||
CollectionProxy.new(options[:database] || database, design_doc, search_name, search_options, self, :search)
|
||||
end
|
||||
|
||||
def parse_view_options(options)
|
||||
design_doc = options.delete(:design_doc)
|
||||
raise ArgumentError, 'design_doc is required' if design_doc.nil?
|
||||
|
||||
view_name = options.delete(:view_name)
|
||||
raise ArgumentError, 'view_name is required' if view_name.nil?
|
||||
|
||||
default_view_options = (design_doc.class == Design &&
|
||||
design_doc['views'][view_name.to_s] &&
|
||||
design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
|
||||
view_options = default_view_options.merge(options)
|
||||
|
||||
[design_doc, view_name, view_options]
|
||||
end
|
||||
|
||||
def parse_search_options(options)
|
||||
design_doc = options.delete(:design_doc)
|
||||
raise ArgumentError, 'design_doc is required' if design_doc.nil?
|
||||
|
||||
search_name = options.delete(:view_name)
|
||||
raise ArgumentError, 'search_name is required' if search_name.nil?
|
||||
|
||||
search_options = options.clone
|
||||
[design_doc, search_name, search_options]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
class CollectionProxy
|
||||
alias_method :proxy_respond_to?, :respond_to?
|
||||
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
|
||||
|
||||
DEFAULT_PAGE = 1
|
||||
DEFAULT_PER_PAGE = 30
|
||||
|
||||
# Create a new CollectionProxy to represent the specified view. If a
|
||||
# container class is specified, the proxy will create an object of the
|
||||
# given type for each row that comes back from the view. If no
|
||||
# container class is specified, the raw results are returned.
|
||||
#
|
||||
# The CollectionProxy provides support for paginating over a collection
|
||||
# via the paginate, and paginated_each methods.
|
||||
def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
|
||||
raise ArgumentError, "database is a required parameter" if database.nil?
|
||||
|
||||
@database = database
|
||||
@container_class = container_class
|
||||
@query_type = query_type
|
||||
|
||||
strip_pagination_options(view_options)
|
||||
@view_options = view_options
|
||||
|
||||
if design_doc.class == Design
|
||||
@view_name = "#{design_doc.name}/#{view_name}"
|
||||
else
|
||||
@view_name = "#{design_doc}/#{view_name}"
|
||||
end
|
||||
end
|
||||
|
||||
# See Collection.paginate
|
||||
def paginate(options = {})
|
||||
page, per_page = parse_options(options)
|
||||
results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
|
||||
remember_where_we_left_off(results, page)
|
||||
instances = convert_to_container_array(results)
|
||||
|
||||
begin
|
||||
if Kernel.const_get('WillPaginate')
|
||||
total_rows = results['total_rows'].to_i
|
||||
paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager|
|
||||
pager.replace(instances)
|
||||
end
|
||||
return paginated
|
||||
end
|
||||
rescue NameError
|
||||
# When not using will_paginate, not much we could do about this. :x
|
||||
end
|
||||
return instances
|
||||
end
|
||||
|
||||
# See Collection.paginated_each
|
||||
def paginated_each(options = {}, &block)
|
||||
page, per_page = parse_options(options)
|
||||
|
||||
begin
|
||||
collection = paginate({:page => page, :per_page => per_page})
|
||||
collection.each(&block)
|
||||
page += 1
|
||||
end until collection.size < per_page
|
||||
end
|
||||
|
||||
def respond_to?(*args)
|
||||
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
||||
end
|
||||
|
||||
# Explicitly proxy === because the instance method removal above
|
||||
# doesn't catch it.
|
||||
def ===(other)
|
||||
load_target
|
||||
other === @target
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def method_missing(method, *args)
|
||||
if load_target
|
||||
if block_given?
|
||||
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
||||
else
|
||||
@target.send(method, *args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_target
|
||||
unless loaded?
|
||||
@view_options.merge!({:include_docs => true}) if @query_type == :search
|
||||
results = @database.send(@query_type, @view_name, @view_options)
|
||||
@target = convert_to_container_array(results)
|
||||
end
|
||||
@loaded = true
|
||||
@target
|
||||
end
|
||||
|
||||
def loaded?
|
||||
@loaded
|
||||
end
|
||||
|
||||
def reload
|
||||
reset
|
||||
load_target
|
||||
self unless @target.nil?
|
||||
end
|
||||
|
||||
def reset
|
||||
@loaded = false
|
||||
@target = nil
|
||||
end
|
||||
|
||||
def inspect
|
||||
load_target
|
||||
@target.inspect
|
||||
end
|
||||
|
||||
def convert_to_container_array(results)
|
||||
if @container_class.nil?
|
||||
results
|
||||
else
|
||||
results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_options(page, per_page)
|
||||
view_options = @view_options.clone
|
||||
if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
|
||||
key = view_options.delete(:key)
|
||||
end_key = view_options[:endkey] || key
|
||||
options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
|
||||
else
|
||||
options = { :limit => per_page, :skip => per_page * (page - 1) }
|
||||
end
|
||||
view_options.merge(options)
|
||||
end
|
||||
|
||||
def parse_options(options)
|
||||
page = options.delete(:page) || DEFAULT_PAGE
|
||||
per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
|
||||
[page.to_i, per_page.to_i]
|
||||
end
|
||||
|
||||
def strip_pagination_options(options)
|
||||
parse_options(options)
|
||||
end
|
||||
|
||||
def remember_where_we_left_off(results, page)
|
||||
last_row = results['rows'].last
|
||||
if last_row
|
||||
@last_key = last_row['key']
|
||||
@last_docid = last_row['id']
|
||||
end
|
||||
@last_page = page
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
126
lib/couchrest/model/design_doc.rb
Normal file
126
lib/couchrest/model/design_doc.rb
Normal file
|
@ -0,0 +1,126 @@
|
|||
# encoding: utf-8
|
||||
module CouchRest
|
||||
module Model
|
||||
module DesignDoc
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
def design_doc
|
||||
@design_doc ||= Design.new(default_design_doc)
|
||||
end
|
||||
|
||||
# Use when something has been changed, like a view, so that on the next request
|
||||
# the design docs will be updated (if changed!)
|
||||
def req_design_doc_refresh
|
||||
@design_doc_fresh = { }
|
||||
end
|
||||
|
||||
def design_doc_id
|
||||
"_design/#{design_doc_slug}"
|
||||
end
|
||||
|
||||
def design_doc_slug
|
||||
self.to_s
|
||||
end
|
||||
|
||||
def default_design_doc
|
||||
{
|
||||
"_id" => design_doc_id,
|
||||
"language" => "javascript",
|
||||
"views" => {
|
||||
'all' => {
|
||||
'map' => "function(doc) {
|
||||
if (doc['couchrest-type'] == '#{self.to_s}') {
|
||||
emit(doc['_id'],1);
|
||||
}
|
||||
}"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# DEPRECATED
|
||||
# use stored_design_doc to retrieve the current design doc
|
||||
def all_design_doc_versions(db = database)
|
||||
db.documents :startkey => "_design/#{self.to_s}",
|
||||
:endkey => "_design/#{self.to_s}-\u9999"
|
||||
end
|
||||
|
||||
# Retreive the latest version of the design document directly
|
||||
# from the database.
|
||||
def stored_design_doc(db = database)
|
||||
db.get(design_doc_id) rescue nil
|
||||
end
|
||||
alias :model_design_doc :stored_design_doc
|
||||
|
||||
def refresh_design_doc(db = database)
|
||||
raise "Database missing for design document refresh" if db.nil?
|
||||
unless design_doc_fresh(db)
|
||||
save_design_doc(db)
|
||||
design_doc_fresh(db, true)
|
||||
end
|
||||
end
|
||||
|
||||
# Save the design doc onto a target database in a thread-safe way,
|
||||
# not modifying the model's design_doc
|
||||
#
|
||||
# See also save_design_doc! to always save the design doc even if there
|
||||
# are no changes.
|
||||
def save_design_doc(db = database, force = false)
|
||||
update_design_doc(Design.new(design_doc), db, force)
|
||||
end
|
||||
|
||||
# Force the update of the model's design_doc even if it hasn't changed.
|
||||
def save_design_doc!(db = database)
|
||||
save_design_doc(db, true)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def design_doc_fresh(db, fresh = nil)
|
||||
@design_doc_fresh ||= {}
|
||||
if fresh.nil?
|
||||
@design_doc_fresh[db.uri] || false
|
||||
else
|
||||
@design_doc_fresh[db.uri] = fresh
|
||||
end
|
||||
end
|
||||
|
||||
# Writes out a design_doc to a given database, returning the
|
||||
# updated design doc
|
||||
def update_design_doc(design_doc, db, force = false)
|
||||
saved = stored_design_doc(db)
|
||||
if saved
|
||||
changes = force
|
||||
design_doc['views'].each do |name, view|
|
||||
if !compare_views(saved['views'][name], view)
|
||||
changes = true
|
||||
saved['views'][name] = view
|
||||
end
|
||||
end
|
||||
if changes
|
||||
db.save_doc(saved)
|
||||
end
|
||||
design_doc
|
||||
else
|
||||
design_doc.database = db
|
||||
design_doc.save
|
||||
design_doc
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if the two views match
|
||||
def compare_views(orig, repl)
|
||||
return false if orig.nil? or repl.nil?
|
||||
(orig['map'].to_s.strip == repl['map'].to_s.strip) && (orig['reduce'].to_s.strip == repl['reduce'].to_s.strip)
|
||||
end
|
||||
|
||||
end # module ClassMethods
|
||||
|
||||
end
|
||||
end
|
||||
end
|
82
lib/couchrest/model/document_queries.rb
Normal file
82
lib/couchrest/model/document_queries.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
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)
|
||||
view(:all, opts, &block)
|
||||
end
|
||||
|
||||
# Returns the number of documents that have the "couchrest-type" field
|
||||
# equal to the name of the current class. Takes the standard set of
|
||||
# CouchRest::Database#view options
|
||||
def count(opts = {}, &block)
|
||||
all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
|
||||
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
|
||||
|
||||
# Load a document from the database by id
|
||||
# No exceptions will be raised if the document isn't found
|
||||
#
|
||||
# ==== Returns
|
||||
# Object:: if the document was found
|
||||
# or
|
||||
# Nil::
|
||||
#
|
||||
# === Parameters
|
||||
# id<String, Integer>:: Document ID
|
||||
# db<Database>:: optional option to pass a custom database to use
|
||||
def get(id, db = database)
|
||||
begin
|
||||
get!(id, db)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
alias :find :get
|
||||
|
||||
# Load a document from the database by id
|
||||
# An exception will be raised if the document isn't found
|
||||
#
|
||||
# ==== Returns
|
||||
# Object:: if the document was found
|
||||
# or
|
||||
# Exception
|
||||
#
|
||||
# === Parameters
|
||||
# id<String, Integer>:: Document ID
|
||||
# db<Database>:: optional option to pass a custom database to use
|
||||
def get!(id, db = database)
|
||||
doc = db.get id
|
||||
create_from_database(doc)
|
||||
end
|
||||
alias :find! :get!
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
23
lib/couchrest/model/errors.rb
Normal file
23
lib/couchrest/model/errors.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# encoding: utf-8
|
||||
module CouchRest
|
||||
module Model
|
||||
module Errors
|
||||
|
||||
class CouchRestModelError < StandardError; end
|
||||
|
||||
# Raised when a persisence method ending in ! fails validation. The message
|
||||
# will contain the full error messages from the +Document+ in question.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# <tt>Validations.new(person.errors)</tt>
|
||||
class Validations < CouchRestModelError
|
||||
attr_reader :document
|
||||
def initialize(document)
|
||||
@document = document
|
||||
super("Validation Failed: #{@document.errors.full_messages.join(", ")}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
73
lib/couchrest/model/extended_attachments.rb
Normal file
73
lib/couchrest/model/extended_attachments.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module ExtendedAttachments
|
||||
|
||||
# Add a file attachment to the current document. Expects
|
||||
# :file and :name to be included in the arguments.
|
||||
def create_attachment(args={})
|
||||
raise ArgumentError unless args[:file] && args[:name]
|
||||
return if has_attachment?(args[:name])
|
||||
self['_attachments'] ||= {}
|
||||
set_attachment_attr(args)
|
||||
rescue ArgumentError => e
|
||||
raise ArgumentError, 'You must specify :file and :name'
|
||||
end
|
||||
|
||||
# reads the data from an attachment
|
||||
def read_attachment(attachment_name)
|
||||
database.fetch_attachment(self, attachment_name)
|
||||
end
|
||||
|
||||
# modifies a file attachment on the current doc
|
||||
def update_attachment(args={})
|
||||
raise ArgumentError unless args[:file] && args[:name]
|
||||
return unless has_attachment?(args[:name])
|
||||
delete_attachment(args[:name])
|
||||
set_attachment_attr(args)
|
||||
rescue ArgumentError => e
|
||||
raise ArgumentError, 'You must specify :file and :name'
|
||||
end
|
||||
|
||||
# deletes a file attachment from the current doc
|
||||
def delete_attachment(attachment_name)
|
||||
return unless self['_attachments']
|
||||
self['_attachments'].delete attachment_name
|
||||
end
|
||||
|
||||
# returns true if attachment_name exists
|
||||
def has_attachment?(attachment_name)
|
||||
!!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
|
||||
end
|
||||
|
||||
# returns URL to fetch the attachment from
|
||||
def attachment_url(attachment_name)
|
||||
return unless has_attachment?(attachment_name)
|
||||
"#{database.root}/#{self.id}/#{attachment_name}"
|
||||
end
|
||||
|
||||
# returns URI to fetch the attachment from
|
||||
def attachment_uri(attachment_name)
|
||||
return unless has_attachment?(attachment_name)
|
||||
"#{database.uri}/#{self.id}/#{attachment_name}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_mime_type(path)
|
||||
return nil if path.nil?
|
||||
type = ::MIME::Types.type_for(path)
|
||||
type.empty? ? nil : type.first.content_type
|
||||
end
|
||||
|
||||
def set_attachment_attr(args)
|
||||
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
|
||||
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
|
||||
self['_attachments'][args[:name]] = {
|
||||
'content_type' => content_type,
|
||||
'data' => args[:file].read
|
||||
}
|
||||
end
|
||||
|
||||
end # module ExtendedAttachments
|
||||
end
|
||||
end
|
141
lib/couchrest/model/persistence.rb
Normal file
141
lib/couchrest/model/persistence.rb
Normal file
|
@ -0,0 +1,141 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Persistence
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Create the document. Validation is enabled by default and will return
|
||||
# false if the document is not valid. If all goes well, the document will
|
||||
# be returned.
|
||||
def create(options = {})
|
||||
return false unless perform_validations(options)
|
||||
_run_create_callbacks do
|
||||
_run_save_callbacks do
|
||||
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
||||
result = database.save_doc(self)
|
||||
(result["ok"] == true) ? self : false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Creates the document in the db. Raises an exception
|
||||
# if the document is not created properly.
|
||||
def create!
|
||||
self.class.fail_validate!(self) unless self.create
|
||||
end
|
||||
|
||||
# Trigger the callbacks (before, after, around)
|
||||
# only if the document isn't new
|
||||
def update(options = {})
|
||||
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
|
||||
return false unless perform_validations(options)
|
||||
_run_update_callbacks do
|
||||
_run_save_callbacks do
|
||||
result = database.save_doc(self)
|
||||
result["ok"] == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Trigger the callbacks (before, after, around) and save the document
|
||||
def save(options = {})
|
||||
self.new? ? create(options) : update(options)
|
||||
end
|
||||
|
||||
# Saves the document to the db using save. Raises an exception
|
||||
# if the document is not saved properly.
|
||||
def save!
|
||||
self.class.fail_validate!(self) unless self.save
|
||||
true
|
||||
end
|
||||
|
||||
# Deletes the document from the database. Runs the :destroy callbacks.
|
||||
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
||||
# document to be saved to a new <tt>_id</tt> if required.
|
||||
def destroy
|
||||
_run_destroy_callbacks do
|
||||
result = database.delete_doc(self)
|
||||
if result['ok']
|
||||
self.delete('_rev')
|
||||
self.delete('_id')
|
||||
end
|
||||
result['ok']
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def perform_validations(options = {})
|
||||
perform_validation = case options
|
||||
when Hash
|
||||
options[:validate] != false
|
||||
else
|
||||
options
|
||||
end
|
||||
perform_validation ? valid? : true
|
||||
end
|
||||
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Creates a new instance, bypassing attribute protection
|
||||
#
|
||||
#
|
||||
# ==== Returns
|
||||
# a document instance
|
||||
def create_from_database(doc = {})
|
||||
base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
|
||||
base.new(doc, :directly_set_attributes => true)
|
||||
end
|
||||
|
||||
# Defines an instance and save it directly to the database
|
||||
#
|
||||
# ==== Returns
|
||||
# returns the reloaded document
|
||||
def create(attributes = {})
|
||||
instance = new(attributes)
|
||||
instance.create
|
||||
instance
|
||||
end
|
||||
|
||||
# Defines an instance and save it directly to the database
|
||||
#
|
||||
# ==== Returns
|
||||
# returns the reloaded document or raises an exception
|
||||
def create!(attributes = {})
|
||||
instance = new(attributes)
|
||||
instance.create!
|
||||
instance
|
||||
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 <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.
|
||||
def unique_id method = nil, &block
|
||||
if method
|
||||
define_method :set_unique_id do
|
||||
self['_id'] ||= self.send(method)
|
||||
end
|
||||
elsif block
|
||||
define_method :set_unique_id do
|
||||
uniqid = block.call(self)
|
||||
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
||||
self['_id'] ||= uniqid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Raise an error if validation failed.
|
||||
def fail_validate!(document)
|
||||
raise Errors::Validations.new(document)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
end
|
144
lib/couchrest/model/properties.rb
Normal file
144
lib/couchrest/model/properties.rb
Normal file
|
@ -0,0 +1,144 @@
|
|||
# encoding: utf-8
|
||||
module CouchRest
|
||||
module Model
|
||||
module Properties
|
||||
|
||||
class IncludeError < StandardError; end
|
||||
|
||||
def self.included(base)
|
||||
base.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
|
||||
self.properties ||= []
|
||||
EOS
|
||||
base.extend(ClassMethods)
|
||||
raise CouchRest::Mixins::Properties::IncludeError, "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
|
||||
end
|
||||
|
||||
# Returns the Class properties
|
||||
#
|
||||
# ==== Returns
|
||||
# Array:: the list of properties for model's class
|
||||
def properties
|
||||
self.class.properties
|
||||
end
|
||||
|
||||
def read_attribute(property)
|
||||
self[property.to_s]
|
||||
end
|
||||
|
||||
def write_attribute(property, value)
|
||||
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
|
||||
raise "Missing property definition for #{property.to_s}" unless prop
|
||||
self[prop.to_s] = prop.cast(self, value)
|
||||
end
|
||||
|
||||
def apply_all_property_defaults
|
||||
return if self.respond_to?(:new?) && (new? == false)
|
||||
# TODO: cache the default object
|
||||
self.class.properties.each do |property|
|
||||
write_attribute(property, property.default_value)
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
def property(name, *options, &block)
|
||||
opts = { }
|
||||
type = options.shift
|
||||
if type.class != Hash
|
||||
opts[:type] = type
|
||||
opts.merge!(options.shift || {})
|
||||
else
|
||||
opts.update(type)
|
||||
end
|
||||
existing_property = self.properties.find{|p| p.name == name.to_s}
|
||||
if existing_property.nil? || (existing_property.default != opts[:default])
|
||||
define_property(name, opts, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
def timestamps!
|
||||
class_eval <<-EOS, __FILE__, __LINE__
|
||||
property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
|
||||
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
|
||||
|
||||
set_callback :save, :before do |object|
|
||||
write_attribute('updated_at', Time.now)
|
||||
write_attribute('created_at', Time.now) if object.new?
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# This is not a thread safe operation, if you have to set new properties at runtime
|
||||
# make sure a mutex is used.
|
||||
def define_property(name, options={}, &block)
|
||||
# check if this property is going to casted
|
||||
type = options.delete(:type) || options.delete(:cast_as)
|
||||
if block_given?
|
||||
type = Class.new(Hash) do
|
||||
include CastedModel
|
||||
end
|
||||
type.class_eval { yield type }
|
||||
type = [type] # inject as an array
|
||||
end
|
||||
property = Property.new(name, type, options)
|
||||
create_property_getter(property)
|
||||
create_property_setter(property) unless property.read_only == true
|
||||
if property.type_class.respond_to?(:validates_casted_model)
|
||||
validates_casted_model property.name
|
||||
end
|
||||
properties << property
|
||||
property
|
||||
end
|
||||
|
||||
# defines the getter for the property (and optional aliases)
|
||||
def create_property_getter(property)
|
||||
# meth = property.name
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{property.name}
|
||||
read_attribute('#{property.name}')
|
||||
end
|
||||
EOS
|
||||
|
||||
if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
|
||||
class_eval <<-EOS, __FILE__, __LINE__
|
||||
def #{property.name}?
|
||||
value = read_attribute('#{property.name}')
|
||||
!(value.nil? || value == false)
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
||||
if property.alias
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
alias #{property.alias.to_sym} #{property.name.to_sym}
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
# defines the setter for the property (and optional aliases)
|
||||
def create_property_setter(property)
|
||||
property_name = property.name
|
||||
class_eval <<-EOS
|
||||
def #{property_name}=(value)
|
||||
write_attribute('#{property_name}', value)
|
||||
end
|
||||
EOS
|
||||
|
||||
if property.alias
|
||||
class_eval <<-EOS
|
||||
alias #{property.alias.to_sym}= #{property_name.to_sym}=
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
end # module ClassMethods
|
||||
|
||||
end
|
||||
end
|
||||
end
|
96
lib/couchrest/model/property.rb
Normal file
96
lib/couchrest/model/property.rb
Normal file
|
@ -0,0 +1,96 @@
|
|||
# encoding: utf-8
|
||||
module CouchRest::Model
|
||||
class Property
|
||||
|
||||
include ::CouchRest::Model::Typecast
|
||||
|
||||
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
|
||||
|
||||
# Attribute to define.
|
||||
# All Properties are assumed casted unless the type is nil.
|
||||
def initialize(name, type = nil, options = {})
|
||||
@name = name.to_s
|
||||
@casted = true
|
||||
parse_type(type)
|
||||
parse_options(options)
|
||||
self
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
# Cast the provided value using the properties details.
|
||||
def cast(parent, value)
|
||||
return value unless casted
|
||||
if type.is_a?(Array)
|
||||
if value.nil?
|
||||
value = []
|
||||
elsif [Hash, HashWithIndifferentAccess].include?(value.class)
|
||||
# Assume provided as a Hash where key is index!
|
||||
data = value
|
||||
value = [ ]
|
||||
data.keys.sort.each do |k|
|
||||
value << data[k]
|
||||
end
|
||||
elsif value.class != Array
|
||||
raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
|
||||
end
|
||||
arr = value.collect { |data| cast_value(parent, data) }
|
||||
# allow casted_by calls to be passed up chain by wrapping in CastedArray
|
||||
value = type_class != String ? CastedArray.new(arr, self) : arr
|
||||
value.casted_by = parent if value.respond_to?(:casted_by)
|
||||
elsif !value.nil?
|
||||
value = cast_value(parent, value)
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
# Cast an individual value, not an array
|
||||
def cast_value(parent, value)
|
||||
raise "An array inside an array cannot be casted, use CastedModel" if value.is_a?(Array)
|
||||
value = typecast_value(value, self)
|
||||
associate_casted_value_to_parent(parent, value)
|
||||
end
|
||||
|
||||
def default_value
|
||||
return if default.nil?
|
||||
if default.class == Proc
|
||||
default.call
|
||||
else
|
||||
Marshal.load(Marshal.dump(default))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def associate_casted_value_to_parent(parent, value)
|
||||
value.casted_by = parent if value.respond_to?(:casted_by)
|
||||
value
|
||||
end
|
||||
|
||||
def parse_type(type)
|
||||
if type.nil?
|
||||
@casted = false
|
||||
@type = nil
|
||||
@type_class = nil
|
||||
else
|
||||
base = type.is_a?(Array) ? type.first : type
|
||||
base = Object if base.nil?
|
||||
raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
|
||||
@type_class = base
|
||||
@type = type
|
||||
end
|
||||
end
|
||||
|
||||
def parse_options(options)
|
||||
@validation_format = options.delete(:format) if options[:format]
|
||||
@read_only = options.delete(:read_only) if options[:read_only]
|
||||
@alias = options.delete(:alias) if options[:alias]
|
||||
@default = options.delete(:default) unless options[:default].nil?
|
||||
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
|
||||
@options = options
|
||||
end
|
||||
|
||||
end
|
||||
end
|
19
lib/couchrest/model/support/couchrest.rb
Normal file
19
lib/couchrest/model/support/couchrest.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
module CouchRest
|
||||
|
||||
class Database
|
||||
|
||||
alias :delete_old! :delete!
|
||||
def delete!
|
||||
clear_model_fresh_cache
|
||||
delete_old!
|
||||
end
|
||||
|
||||
# If the database is deleted, ensure that the design docs will be refreshed.
|
||||
def clear_model_fresh_cache
|
||||
::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
32
lib/couchrest/model/support/rails.rb
Normal file
32
lib/couchrest/model/support/rails.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# This file contains various hacks for Rails compatibility.
|
||||
class Hash
|
||||
# Hack so that CouchRest::Document, which descends from Hash,
|
||||
# doesn't appear to Rails routing as a Hash of options
|
||||
def self.===(other)
|
||||
return false if self == Hash && other.is_a?(CouchRest::Document)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
CouchRest::Document.class_eval do
|
||||
# Need this when passing doc to a resourceful route
|
||||
alias_method :to_param, :id
|
||||
|
||||
# Hack so that CouchRest::Document, which descends from Hash,
|
||||
# doesn't appear to Rails routing as a Hash of options
|
||||
def is_a?(o)
|
||||
return false if o == Hash
|
||||
super
|
||||
end
|
||||
alias_method :kind_of?, :is_a?
|
||||
end
|
||||
|
||||
CouchRest::Model::CastedModel.class_eval do
|
||||
# The to_param method is needed for rails to generate resourceful routes.
|
||||
# In your controller, remember that it's actually the id of the document.
|
||||
def id
|
||||
return nil if base_doc.nil?
|
||||
base_doc.id
|
||||
end
|
||||
alias_method :to_param, :id
|
||||
end
|
170
lib/couchrest/model/typecast.rb
Normal file
170
lib/couchrest/model/typecast.rb
Normal file
|
@ -0,0 +1,170 @@
|
|||
class Time
|
||||
# returns a local time value much faster than Time.parse
|
||||
def self.mktime_with_offset(string)
|
||||
string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
|
||||
# $1 = year
|
||||
# $2 = month
|
||||
# $3 = day
|
||||
# $4 = hours
|
||||
# $5 = minutes
|
||||
# $6 = seconds
|
||||
# $7 = time zone direction
|
||||
# $8 = tz difference
|
||||
# utc time with wrong TZ info:
|
||||
time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
|
||||
tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
|
||||
time + tz_difference + zone_offset(time.zone)
|
||||
end
|
||||
end
|
||||
|
||||
module CouchRest
|
||||
module Model
|
||||
module Typecast
|
||||
|
||||
def typecast_value(value, property) # klass, init_method)
|
||||
return nil if value.nil?
|
||||
klass = property.type_class
|
||||
if value.instance_of?(klass) || klass == Object
|
||||
value
|
||||
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
||||
send('typecast_to_'+klass.to_s.downcase, value)
|
||||
else
|
||||
# Allow the init_method to be defined as a Proc for advanced conversion
|
||||
property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Typecast a value to an Integer
|
||||
def typecast_to_integer(value)
|
||||
typecast_to_numeric(value, :to_i)
|
||||
end
|
||||
|
||||
# Typecast a value to a String
|
||||
def typecast_to_string(value)
|
||||
value.to_s
|
||||
end
|
||||
|
||||
# Typecast a value to a true or false
|
||||
def typecast_to_trueclass(value)
|
||||
if value.kind_of?(Integer)
|
||||
return true if value == 1
|
||||
return false if value == 0
|
||||
elsif value.respond_to?(:to_s)
|
||||
return true if %w[ true 1 t ].include?(value.to_s.downcase)
|
||||
return false if %w[ false 0 f ].include?(value.to_s.downcase)
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
# Typecast a value to a BigDecimal
|
||||
def typecast_to_bigdecimal(value)
|
||||
if value.kind_of?(Integer)
|
||||
value.to_s.to_d
|
||||
else
|
||||
typecast_to_numeric(value, :to_d)
|
||||
end
|
||||
end
|
||||
|
||||
# Typecast a value to a Float
|
||||
def typecast_to_float(value)
|
||||
typecast_to_numeric(value, :to_f)
|
||||
end
|
||||
|
||||
# Match numeric string
|
||||
def typecast_to_numeric(value, method)
|
||||
if value.respond_to?(:to_str)
|
||||
if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
|
||||
$1.send(method)
|
||||
else
|
||||
value
|
||||
end
|
||||
elsif value.respond_to?(method)
|
||||
value.send(method)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Typecasts an arbitrary value to a DateTime.
|
||||
# Handles both Hashes and DateTime instances.
|
||||
# This is slow!! Use Time instead.
|
||||
def typecast_to_datetime(value)
|
||||
if value.is_a?(Hash)
|
||||
typecast_hash_to_datetime(value)
|
||||
else
|
||||
DateTime.parse(value.to_s)
|
||||
end
|
||||
rescue ArgumentError
|
||||
value
|
||||
end
|
||||
|
||||
# Typecasts an arbitrary value to a Date
|
||||
# Handles both Hashes and Date instances.
|
||||
def typecast_to_date(value)
|
||||
if value.is_a?(Hash)
|
||||
typecast_hash_to_date(value)
|
||||
elsif value.is_a?(Time) # sometimes people think date is time!
|
||||
value.to_date
|
||||
elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
|
||||
# Faster than parsing the date
|
||||
Date.new($1.to_i, $2.to_i, $3.to_i)
|
||||
else
|
||||
Date.parse(value)
|
||||
end
|
||||
rescue ArgumentError
|
||||
value
|
||||
end
|
||||
|
||||
# Typecasts an arbitrary value to a Time
|
||||
# Handles both Hashes and Time instances.
|
||||
def typecast_to_time(value)
|
||||
if value.is_a?(Hash)
|
||||
typecast_hash_to_time(value)
|
||||
else
|
||||
Time.mktime_with_offset(value.to_s)
|
||||
end
|
||||
rescue ArgumentError
|
||||
value
|
||||
rescue TypeError
|
||||
value
|
||||
end
|
||||
|
||||
# Creates a DateTime instance from a Hash with keys :year, :month, :day,
|
||||
# :hour, :min, :sec
|
||||
def typecast_hash_to_datetime(value)
|
||||
DateTime.new(*extract_time(value))
|
||||
end
|
||||
|
||||
# Creates a Date instance from a Hash with keys :year, :month, :day
|
||||
def typecast_hash_to_date(value)
|
||||
Date.new(*extract_time(value)[0, 3])
|
||||
end
|
||||
|
||||
# Creates a Time instance from a Hash with keys :year, :month, :day,
|
||||
# :hour, :min, :sec
|
||||
def typecast_hash_to_time(value)
|
||||
Time.local(*extract_time(value))
|
||||
end
|
||||
|
||||
# Extracts the given args from the hash. If a value does not exist, it
|
||||
# uses the value of Time.now.
|
||||
def extract_time(value)
|
||||
now = Time.now
|
||||
[:year, :month, :day, :hour, :min, :sec].map do |segment|
|
||||
typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
|
||||
end
|
||||
end
|
||||
|
||||
# Typecast a value to a Class
|
||||
def typecast_to_class(value)
|
||||
value.to_s.constantize
|
||||
rescue NameError
|
||||
value
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
32
lib/couchrest/model/validations.rb
Normal file
32
lib/couchrest/model/validations.rb
Normal file
|
@ -0,0 +1,32 @@
|
|||
# encoding: utf-8
|
||||
|
||||
require "couchrest/model/validations/casted_model"
|
||||
|
||||
module CouchRest
|
||||
module Model
|
||||
|
||||
# Validations may be applied to both Model::Base and Model::CastedModel
|
||||
module Validations
|
||||
extend ActiveSupport::Concern
|
||||
included do
|
||||
include ActiveModel::Validations
|
||||
end
|
||||
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# Validates the associated casted model. This method should not be
|
||||
# used within your code as it is automatically included when a CastedModel
|
||||
# is used inside the model.
|
||||
#
|
||||
def validates_casted_model(*args)
|
||||
validates_with(CastedModelValidator, _merge_attributes(args))
|
||||
end
|
||||
|
||||
# TODO: Here will lie validates_uniqueness_of
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
14
lib/couchrest/model/validations/casted_model.rb
Normal file
14
lib/couchrest/model/validations/casted_model.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Validations
|
||||
class CastedModelValidator < ActiveModel::EachValidator
|
||||
|
||||
def validate_each(document, attribute, value)
|
||||
values = value.is_a?(Array) ? value : [value]
|
||||
return if values.collect {|doc| doc.nil? || doc.valid? }.all?
|
||||
document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
166
lib/couchrest/model/views.rb
Normal file
166
lib/couchrest/model/views.rb
Normal file
|
@ -0,0 +1,166 @@
|
|||
module CouchRest
|
||||
module Model
|
||||
module Views
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
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.
|
||||
#
|
||||
# Calling with :database => [instance of CouchRest::Database] will
|
||||
# send the query to a specific database, otherwise it will go to
|
||||
# the model's default database (use_database)
|
||||
#
|
||||
# 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 completely,
|
||||
# it is recommended that you read the RSpec file at
|
||||
# <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
|
||||
|
||||
def view_by(*keys)
|
||||
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
|
||||
design_doc.view_by(*keys)
|
||||
req_design_doc_refresh
|
||||
end
|
||||
|
||||
# returns stored defaults if 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)
|
||||
db = query.delete(:database) || database
|
||||
refresh_design_doc(db)
|
||||
query[:raw] = true if query[:reduce]
|
||||
raw = query.delete(:raw)
|
||||
fetch_view_with_docs(db, name, query, raw, &block)
|
||||
end
|
||||
|
||||
# Find the first entry in the view. If the second parameter is a string
|
||||
# it will be used as the key for the request, for example:
|
||||
#
|
||||
# Course.first_from_view('by_teacher', 'Fred')
|
||||
#
|
||||
# More advanced requests can be performed by providing a hash:
|
||||
#
|
||||
# Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
|
||||
#
|
||||
def first_from_view(name, *args)
|
||||
query = {:limit => 1}
|
||||
case args.first
|
||||
when String, Array
|
||||
query.update(args[1]) unless args[1].nil?
|
||||
query[:key] = args.first
|
||||
when Hash
|
||||
query.update(args.first)
|
||||
end
|
||||
view(name, query).first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_view_with_docs(db, name, opts, raw=false, &block)
|
||||
if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
|
||||
fetch_view(db, name, opts, &block)
|
||||
else
|
||||
begin
|
||||
if block.nil?
|
||||
collection_proxy_for(design_doc, name, opts.merge({:include_docs => true}))
|
||||
else
|
||||
view = fetch_view db, name, opts.merge({:include_docs => true}), &block
|
||||
view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
|
||||
end
|
||||
rescue
|
||||
# fallback for old versions of couchdb that don't
|
||||
# have include_docs support
|
||||
view = fetch_view(db, name, opts, &block)
|
||||
view['rows'].collect{|r|create_from_database(db.get(r['id']))} if view['rows']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_view(db, view_name, opts, &block)
|
||||
raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
|
||||
retryable = true
|
||||
begin
|
||||
design_doc.view_on(db, view_name, opts, &block)
|
||||
# the design doc may not have been saved yet on this database
|
||||
rescue RestClient::ResourceNotFound => e
|
||||
if retryable
|
||||
save_design_doc(db)
|
||||
retryable = false
|
||||
retry
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # module ClassMethods
|
||||
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue