Merge remote branch 'refs/remotes/canonical/master' into design-mapper-more-auto-update-design-doc-aware

This commit is contained in:
Peter Williams 2011-11-29 08:22:30 -07:00
commit 88bb413ec2
66 changed files with 866 additions and 423 deletions

View file

@ -183,19 +183,19 @@ module CouchRest
casted_by[casted_by_property.to_s] << obj.id
end
end
def << obj
check_obj(obj)
casted_by[casted_by_property.to_s] << obj.id
super(obj)
end
def push(obj)
check_obj(obj)
casted_by[casted_by_property.to_s].push obj.id
super(obj)
end
def unshift(obj)
check_obj(obj)
casted_by[casted_by_property.to_s].unshift obj.id
@ -212,7 +212,7 @@ module CouchRest
casted_by[casted_by_property.to_s].pop
super
end
def shift
casted_by[casted_by_property.to_s].shift
super

View file

@ -1,13 +1,12 @@
module CouchRest
module Model
class Base < Document
class Base < CouchRest::Document
extend ActiveModel::Naming
include CouchRest::Model::Configuration
include CouchRest::Model::Connection
include CouchRest::Model::Persistence
include CouchRest::Model::Callbacks
include CouchRest::Model::DocumentQueries
include CouchRest::Model::Views
include CouchRest::Model::DesignDoc
@ -18,9 +17,11 @@ module CouchRest
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Callbacks
include CouchRest::Model::Designs
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
include CouchRest::Model::Callbacks
def self.subclasses
@subclasses ||= []
@ -46,22 +47,24 @@ module CouchRest
#
# Options supported:
#
# * :directly_set_attributes: true when data comes directly from database
# * :database: provide an alternative database
# * :directly_set_attributes, true when data comes directly from database
# * :database, provide an alternative database
#
# If a block is provided the new model will be passed into the
# block so that it can be populated.
def initialize(doc = {}, options = {})
doc = prepare_all_attributes(doc, options)
# set the instances database, if provided
def initialize(attributes = {}, options = {})
super()
prepare_all_attributes(attributes, options)
# set the instance's 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
end
yield self if block_given?
after_initialize if respond_to?(:after_initialize)
run_callbacks(:initialize) { self }
end
@ -79,18 +82,19 @@ module CouchRest
super
end
## Compatibility with ActiveSupport and older frameworks
# Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options
def is_a?(klass)
return false if klass == Hash
super
# compatbility for 1.8, it does not use respond_to_missing?
# thing is, when using it like this only, doing method(:find_by_view)
# will throw an error
def self.respond_to?(m, include_private = false)
super || respond_to_missing?(m, include_private)
end
alias :kind_of? :is_a?
def persisted?
!new?
# ruby 1.9 feature
# this allows ruby to know that the method is defined using
# method_missing, and as such, method(:find_by_view) will actually
# give a Method back, and not throw an error like in 1.8!
def self.respond_to_missing?(m, include_private = false)
has_view?(m) || has_view?(m.to_s[/^find_(by_.+)/, 1])
end
def to_key
@ -100,6 +104,26 @@ module CouchRest
alias :to_param :id
alias :new_record? :new?
alias :new_document? :new?
# Compare this model with another by confirming to see
# if the IDs and their databases match!
#
# Camparison of the database is required in case the
# model has been proxied or loaded elsewhere.
#
# A Basic CouchRest document will only ever compare using
# a Hash comparison on the attributes.
def == other
return false unless other.is_a?(Base)
if id.nil? && other.id.nil?
# no ids? assume comparing nested and revert to hash comparison
to_hash == other.to_hash
else
database == other.database && id == other.id
end
end
alias :eql? :==
end
end
end

View file

@ -5,21 +5,23 @@ module CouchRest #:nodoc:
module Callbacks
extend ActiveSupport::Concern
CALLBACKS = [
:before_validation, :after_validation,
:after_initialize,
:before_create, :around_create, :after_create,
:before_destroy, :around_destroy, :after_destroy,
:before_save, :around_save, :after_save,
:before_update, :around_update, :after_update,
]
included do
extend ActiveModel::Callbacks
include ActiveModel::Validations::Callbacks
define_model_callbacks \
:create,
:destroy,
:save,
:update
define_model_callbacks :initialize, :only => :after
define_model_callbacks :create, :destroy, :save, :update
end
def valid?(*) #nodoc
_run_validation_callbacks { super }
end
end
end

View file

@ -50,6 +50,16 @@ module CouchRest::Model
super
end
def delete(obj)
couchrest_parent_will_change! if use_dirty? && self.length > 0
super(obj)
end
def delete_at(index)
couchrest_parent_will_change! if use_dirty? && self.length > 0
super(index)
end
def build(*args)
obj = casted_by_property.build(*args)
self.push(obj)

View file

@ -244,6 +244,7 @@ module CouchRest
else
options = { :limit => per_page, :skip => per_page * (page - 1) }
end
options[:include_docs] = true
view_options.merge(options)
end

View file

@ -1,13 +1,10 @@
module CouchRest
module Model
module DocumentQueries
def self.included(base)
base.extend(ClassMethods)
end
extend ActiveSupport::Concern
module ClassMethods
# Load all documents that have the model_type_key's field equal to the
# name of the current class. Take the standard set of
# CouchRest::Database#view options.
@ -73,7 +70,7 @@ module CouchRest
end
end
alias :find :get
# Load a document from the database by id
# An exception will be raised if the document isn't found
#

View file

@ -1,37 +1,37 @@
module CouchRest::Model
module CastedModel
module Embeddable
extend ActiveSupport::Concern
# Include Attributes early to ensure super() will work
include CouchRest::Attributes
included do
include CouchRest::Model::Configuration
include CouchRest::Model::Callbacks
include CouchRest::Model::Properties
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Callbacks
include CouchRest::Model::CastedBy
include CouchRest::Model::Dirty
include CouchRest::Model::Callbacks
class_eval do
# Override CastedBy's base_doc?
def base_doc?
false # Can never be base doc!
end
end
end
def initialize(keys = {})
raise StandardError unless self.is_a? Hash
prepare_all_attributes(keys)
# Initialize a new Casted Model. Accepts the same
# options as CouchRest::Model::Base for preparing and initializing
# attributes.
def initialize(keys = {}, options = {})
super()
end
def []= key, value
super(key.to_s, value)
end
def [] key
super(key.to_s)
prepare_all_attributes(keys, options)
run_callbacks(:initialize) { self }
end
# False if the casted model has already
@ -65,6 +65,14 @@ module CouchRest::Model
end
alias :attributes= :update_attributes_without_saving
end # End Embeddable
# Provide backwards compatability with previous versions (pre 1.1.0)
module CastedModel
extend ActiveSupport::Concern
included do
include CouchRest::Model::Embeddable
end
end
end

View file

@ -21,14 +21,15 @@ module CouchRest
# 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
def create!(options = {})
self.class.fail_validate!(self) unless self.create(options)
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?
raise "Cannot save a destroyed document!" if destroyed?
raise "Calling #{self.class.name}#update on document that has not been created!" if new?
return false unless perform_validations(options)
return true if !self.disable_dirty && !self.changed?
_run_update_callbacks do
@ -54,19 +55,25 @@ module CouchRest
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')
@_destroyed = true
self.freeze
end
result['ok']
end
end
def destroyed?
!!@_destroyed
end
def persisted?
!new? && !destroyed?
end
# Update the document's attributes and save. For example:
#
# doc.update_attributes :name => "Fred"
@ -85,7 +92,7 @@ module CouchRest
#
# Returns self.
def reload
merge!(self.class.get(id))
prepare_all_attributes(database.get(id), :directly_set_attributes => true)
self
end
@ -104,22 +111,25 @@ module CouchRest
module ClassMethods
# Creates a new instance, bypassing attribute protection
# Creates a new instance, bypassing attribute protection and
# uses the type field to determine which model to use to instanatiate
# the new object.
#
# ==== Returns
# a document instance
#
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)
def build_from_database(doc = {}, options = {}, &block)
src = doc[model_type_key]
base = (src.blank? || src == self.to_s) ? self : src.constantize
base.new(doc, options.merge(:directly_set_attributes => true), &block)
end
# Defines an instance and save it directly to the database
#
# ==== Returns
# returns the reloaded document
def create(attributes = {})
instance = new(attributes)
def create(attributes = {}, &block)
instance = new(attributes, &block)
instance.create
instance
end
@ -128,8 +138,8 @@ module CouchRest
#
# ==== Returns
# returns the reloaded document or raises an exception
def create!(attributes = {})
instance = new(attributes)
def create!(attributes = {}, &block)
instance = new(attributes, &block)
instance.create!
instance
end

View file

@ -12,8 +12,10 @@ module CouchRest
raise "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 (method_defined?(:[]) && method_defined?(:[]=))
end
def as_json(options = nil)
Hash[self].reject{|k,v| v.nil?}.as_json(options)
# Provide an attribute hash ready to be sent to CouchDB but with
# all the nil attributes removed.
def as_couch_json
super.delete_if{|k,v| v.nil?}
end
# Returns the Class properties with their values
@ -80,17 +82,18 @@ module CouchRest
self.disable_dirty = dirty
end
def prepare_all_attributes(doc = {}, options = {})
def prepare_all_attributes(attrs = {}, options = {})
self.disable_dirty = !!options[:directly_set_attributes]
apply_all_property_defaults
if options[:directly_set_attributes]
directly_set_read_only_attributes(doc)
directly_set_read_only_attributes(attrs)
directly_set_attributes(attrs, true)
else
doc = remove_protected_attributes(doc)
attrs = remove_protected_attributes(attrs)
directly_set_attributes(attrs)
end
res = doc.nil? ? doc : directly_set_attributes(doc)
self.disable_dirty = false
res
self
end
def find_property!(property)
@ -101,16 +104,13 @@ module CouchRest
# Set all the attributes and return a hash with the attributes
# that have not been accepted.
def directly_set_attributes(hash)
hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value)
true
elsif mass_assign_any_attribute # config option
self[attribute_name] = attribute_value
true
else
false
def directly_set_attributes(hash, mass_assign = false)
return if hash.nil?
hash.reject do |key, value|
if self.respond_to?("#{key}=")
self.send("#{key}=", value)
elsif mass_assign || mass_assign_any_attribute
self[key] = value
end
end
end
@ -151,15 +151,13 @@ module CouchRest
# These properties are casted as Time objects, so they should always
# be set to UTC.
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)
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
set_callback :save, :before do |object|
write_attribute('updated_at', Time.now)
write_attribute('created_at', Time.now) if object.new?
end
end
protected
@ -170,8 +168,8 @@ module CouchRest
# 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
type = Class.new do
include Embeddable
end
if block.arity == 1 # Traditional, with options
type.class_eval { yield type }
@ -193,42 +191,32 @@ module CouchRest
# 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
define_method(property.name) do
read_attribute(property.name)
end
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
define_method("#{property.name}?") do
value = read_attribute(property.name)
!(value.nil? || value == false)
end
end
if property.alias
class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym}
EOS
alias_method(property.alias, property.name.to_sym)
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
name = property.name
define_method("#{name}=") do |value|
write_attribute(name, value)
end
if property.alias
class_eval <<-EOS
alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS
alias_method "#{property.alias}=", "#{name}="
end
end

View file

@ -27,19 +27,15 @@ module CouchRest::Model
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
# Assume provided as a params hash where key is index
value = parameter_hash_to_array(value)
elsif !value.is_a?(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
CastedArray.new(arr, self, parent)
elsif (type == Object || type == Hash) && (value.class == Hash)
elsif (type == Object || type == Hash) && (value.is_a?(Hash))
# allow casted_by calls to be passed up chain by wrapping in CastedHash
CastedHash[value, self, parent]
elsif !value.nil?
@ -47,9 +43,8 @@ module CouchRest::Model
end
end
# Cast an individual value, not an array
# Cast an individual value
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
@ -78,6 +73,14 @@ module CouchRest::Model
private
def parameter_hash_to_array(source)
value = [ ]
source.keys.each do |k|
value[k.to_i] = source[k]
end
value.compact
end
def associate_casted_value_to_parent(parent, value)
value.casted_by = parent if value.respond_to?(:casted_by)
value.casted_by_property = self if value.respond_to?(:casted_by_property)

View file

@ -73,12 +73,12 @@ module CouchRest
end
# Base
def new(*args)
proxy_update(model.new(*args))
def new(attrs = {}, options = {}, &block)
proxy_block_update(:new, attrs, options, &block)
end
def build_from_database(doc = {})
proxy_update(model.build_from_database(doc))
def build_from_database(attrs = {}, options = {}, &block)
proxy_block_update(:build_from_database, attrs, options, &block)
end
def method_missing(m, *args, &block)
@ -170,6 +170,13 @@ module CouchRest
end
end
def proxy_block_update(method, *args, &block)
model.send(method, *args) do |doc|
proxy_update(doc)
yield doc if block_given?
end
end
end
end
end

View file

@ -18,7 +18,7 @@ CouchRest::Design.class_eval do
flatten =
lambda {|r|
(recurse = lambda {|v|
if v.is_a?(Hash)
if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
v.to_a.map{|v| recurse.call(v)}.flatten
elsif v.is_a?(Array)
v.flatten.map{|v| recurse.call(v)}

View file

@ -13,22 +13,33 @@ module CouchRest
# Validations may be applied to both Model::Base and Model::CastedModel
module Validations
extend ActiveSupport::Concern
included do
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
include ActiveModel::Validations
# Determine if the document is valid.
#
# @example Is the document valid?
# person.valid?
#
# @example Is the document valid in a context?
# person.valid?(:create)
#
# @param [ Symbol ] context The optional validation context.
#
# @return [ true, false ] True if valid, false if not.
#
def valid?(context = nil)
super context ? context : (new? ? :create : :update)
end
module ClassMethods
# Validates the associated casted model. This method should not be
# 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
# Validates if the field is unique for this type of document. Automatically creates
# a view if one does not already exist and performs a search for all matching
# documents.