Adding support for proxying and more refinements to views

This commit is contained in:
Sam Lown 2011-02-09 21:21:03 +01:00
parent 63bb1bb6bd
commit a78e3b74d6
14 changed files with 560 additions and 46 deletions

View file

@ -109,7 +109,7 @@ module CouchRest
base = options[:proxy] || options[:class_name]
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]})
@#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]})
end
EOS
end
@ -140,7 +140,7 @@ module CouchRest
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)}
ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)}
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
end
EOS

View file

@ -12,6 +12,7 @@ module CouchRest
include CouchRest::Model::DesignDoc
include CouchRest::Model::ExtendedAttachments
include CouchRest::Model::ClassProxy
include CouchRest::Model::Proxyable
include CouchRest::Model::Collection
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
@ -46,9 +47,12 @@ module CouchRest
# Options supported:
#
# * :directly_set_attributes: true when data comes directly from database
# * :database: provide an alternative database
#
def initialize(doc = {}, options = {})
doc = prepare_all_attributes(doc, options)
# set the instances database, if provided
self.database = options[:database] unless options[:database].nil?
super(doc)
unless self['_id'] && self['_rev']
self[self.model_type_key] = self.class.to_s

View file

@ -222,7 +222,7 @@ module CouchRest
if @container_class.nil?
results
else
results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
end
end

View file

@ -5,18 +5,19 @@ module CouchRest
#
# A proxy class that allows view queries to be created using
# chained method calls. After each call a new instance of the method
# is created based on the original in a similar fashion to ruby's sequel
# is created based on the original in a similar fashion to ruby's Sequel
# library, or Rails 3's Arel.
#
# CouchDB views have inherent limitations, so joins and filters as used in
# a normal relational database are not possible. At least not yet!
# a normal relational database are not possible.
#
class View
include Enumerable
attr_accessor :model, :name, :query, :result
# Initialize a new View object. This method should not be called from outside CouchRest Model.
# Initialize a new View object. This method should not be called from
# outside CouchRest Model.
def initialize(parent, new_query = {}, name = nil)
if parent.is_a?(Class) && parent < CouchRest::Model::Base
raise "Name must be provided for view to be initialized" if name.nil?
@ -25,14 +26,14 @@ module CouchRest
# Default options:
self.query = { :reduce => false }
elsif parent.is_a?(self.class)
self.model = parent.model
self.model = (new_query.delete(:proxy) || parent.model)
self.name = parent.name
self.query = parent.query.dup
else
raise "View cannot be initialized without a parent Model or View"
end
query.update(new_query)
super
super()
end
@ -110,6 +111,13 @@ module CouchRest
end
end
# Check to see if the array of documents is empty. This *will*
# perform the query and return all documents ready to use, if you don't
# want to load anything, use +#total_rows+ or +#count+ instead.
def empty?
all.empty?
end
# Run through each document provided by the +#all+ method.
# This is also used by the Enumerator mixin to provide all the standard
# ruby collection directly on the view.
@ -141,6 +149,18 @@ module CouchRest
rows.map{|r| r.value}
end
# Accept requests as if the view was an array. Used for backwards compatibity
# with older queries:
#
# Model.all(:raw => true, :limit => 0)['total_rows']
#
# In this example, the raw option will be ignored, and the total rows
# will still be accessible.
#
def [](value)
execute[value]
end
# No yet implemented. Eventually this will provide a raw hash
# of the information CouchDB holds about the view.
def info
@ -150,16 +170,11 @@ module CouchRest
# == View Filter Methods
#
# View filters return an copy of the view instance with the query
# View filters return a copy of the view instance with the query
# modified appropriatly. Errors will be raised if the methods
# are combined in an incorrect fashion.
#
# Specify the database the view should use. If not defined,
# an attempt will be made to load its value from the model.
def database(value)
update_query(:database => value)
end
# Find all entries in the index whose key matches the value provided.
#
@ -229,7 +244,8 @@ module CouchRest
update_query(:skip => value)
end
# Use the reduce function on the view. If none is available this method will fail.
# Use the reduce function on the view. If none is available this method
# will fail.
def reduce
raise "Cannot reduce a view without a reduce method" unless can_reduce?
update_query(:reduce => true)
@ -257,6 +273,25 @@ module CouchRest
update_query.include_docs!
end
### Special View Filter Methods
# Specify the database the view should use. If not defined,
# an attempt will be made to load its value from the model.
def database(value)
update_query(:database => value)
end
# Set the view's proxy that will be used instead of the model
# for any future searches. As soon as this enters the
# new object's initializer it will be removed and replace
# the model object.
#
# See the Proxyable mixin for more details.
#
def proxy(value)
update_query(:proxy => value)
end
# Return any cached values to their nil state so that any queries
# requested later will have a fresh set of data.
def reset!
@ -288,20 +323,22 @@ module CouchRest
def can_reduce?
!design_doc['views'][name]['reduce'].blank?
end
def use_database
query[:database] || model.database
end
def execute
return self.result if result
db = query[:database] || model.database
raise "Database must be defined in model or view!" if db.nil?
raise "Database must be defined in model or view!" if use_database.nil?
retryable = true
# Remove the reduce value if its not needed
query.delete(:reduce) unless can_reduce?
begin
self.result = model.design_doc.view_on(db, name, query)
self.result = model.design_doc.view_on(use_database, name, query)
rescue RestClient::ResourceNotFound => e
if retryable
model.save_design_doc(db)
model.save_design_doc(use_database)
retryable = false
retry
else
@ -334,9 +371,10 @@ module CouchRest
def create(model, name, opts = {})
unless opts[:map]
if opts[:by].nil? && name =~ /^by_(.+)/
if opts[:by].nil? && name.to_s =~ /^by_(.+)/
opts[:by] = $1.split(/_and_/)
end
raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil?
opts[:guards] ||= []
@ -373,9 +411,9 @@ module CouchRest
# A special wrapper class that provides easy access to the key
# fields in a result row.
class ViewRow < Hash
attr_accessor :model
attr_reader :model
def initialize(hash, model)
self.model = model
@model = model
replace(hash)
end
def id
@ -393,7 +431,7 @@ module CouchRest
# Send a request for the linked document either using the "id" field's
# value, or the ["value"]["_id"] used for linked documents.
def doc
return model.create_from_database(self['doc']) if self['doc']
return model.build_from_database(self['doc']) if self['doc']
doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id
model.get(doc_id)
end

View file

@ -88,7 +88,7 @@ module CouchRest
def get!(id, db = database)
raise "Missing or empty document ID" if id.to_s.empty?
doc = db.get id
create_from_database(doc)
build_from_database(doc)
end
alias :find! :get!

View file

@ -93,12 +93,12 @@ module CouchRest
# Creates a new instance, bypassing attribute protection
#
#
# ==== Returns
# a document instance
def create_from_database(doc = {})
#
def build_from_database(doc = {})
base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
base.new(doc, :directly_set_attributes => true)
base.new(doc, :directly_set_attributes => true)
end
# Defines an instance and save it directly to the database

View file

@ -0,0 +1,152 @@
module CouchRest
module Model
# :nodoc: Because I like inventing words
module Proxyable
extend ActiveSupport::Concern
included do
attr_accessor :model_proxy
end
module ClassMethods
# Define a collection that will use the base model for the database connection
# details.
def proxy_for(model_name, options = {})
db_method = options[:database_method] || "proxy_database"
options[:class_name] ||= model_name.to_s.singularize.camelize
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{model_name}
unless respond_to?('#{db_method}')
raise "Missing ##{db_method} method for proxy"
end
@#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(#{options[:class_name]}, self, '#{model_name}', #{db_method})
end
EOS
end
def proxied_by(model_name, options = {})
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name)
attr_accessor model_name
end
end
class ModelProxy
attr_reader :model, :owner, :owner_name, :database
def initialize(model, owner, owner_name, database)
@model = model
@owner = owner
@owner_name = owner_name
@database = database
end
# Base
def new(*args)
proxy_update(model.new(*args))
end
def build_from_database(doc = {})
proxy_update(model.build_from_database(doc))
end
def method_missing(m, *args, &block)
if has_view?(m)
if model.respond_to?(m)
return model.send(m, *args).proxy(self)
else
query = args.shift || {}
return view(m, query, *args, &block)
end
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)
proxy_update_all(@model.all({:database => @database}.merge(opts), &block))
end
def count(opts = {})
@model.count({:database => @database}.merge(opts))
end
def first(opts = {})
proxy_update(@model.first({:database => @database}.merge(opts)))
end
def last(opts = {})
proxy_update(@model.last({:database => @database}.merge(opts)))
end
def get(id)
proxy_update(@model.get(id, @database))
end
alias :find :get
# Views
def has_view?(view)
@model.has_view?(view)
end
def view_by(*args)
@model.view_by(*args)
end
def view(name, query={}, &block)
proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block))
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
proxy_update(@model.first_from_view(name, *args))
end
# DesignDoc
def design_doc
@model.design_doc
end
def refresh_design_doc
@model.refresh_design_doc(@database)
end
def save_design_doc
@model.save_design_doc(@database)
end
protected
# Update the document's proxy details, specifically, the fields that
# link back to the original document.
def proxy_update(doc)
if doc
doc.database = @database if doc.respond_to?(:database=)
doc.model_proxy = self if doc.respond_to?(:model_proxy=)
doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=")
end
doc
end
def proxy_update_all(docs)
docs.each do |doc|
proxy_update(doc)
end
end
end
end
end
end

View file

@ -9,19 +9,19 @@ module CouchRest
# Ensure we have a class available so we can check for a usable view
# or add one if necessary.
def setup(klass)
@klass = klass
def setup(model)
@model = model
end
def validate_each(document, attribute, value)
view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
model = document.model_proxy || @model
# Determine the base of the search
base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy])
base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
if base.respond_to?(:has_view?) && !base.has_view?(view_name)
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
@klass.view_by attribute
model.view_by attribute
end
docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows']
@ -36,7 +36,6 @@ module CouchRest
end
end
end
end

View file

@ -131,7 +131,7 @@ module CouchRest
collection_proxy_for(design_doc, name, opts.merge({:database => db, :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']
view['rows'].collect{|r|build_from_database(r['doc'])} if view['rows']
end
end
end