merged Peter's stuff

This commit is contained in:
Matt Aimonetti 2009-08-24 16:57:58 -07:00
commit 64a51b73ae
20 changed files with 944 additions and 334 deletions

View file

@ -103,24 +103,35 @@ Check spec/couchrest/more and spec/fixtures/more for more examples
save_callback :before, :generate_slug_from_title save_callback :before, :generate_slug_from_title
def generate_slug_from_title def generate_slug_from_title
self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document? self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?
end end
end end
### Callbacks ### Callbacks
`CouchRest::ExtendedDocuments` instances have 2 callbacks already defined for you: `CouchRest::ExtendedDocuments` instances have 4 callbacks already defined for you:
`create_callback`, `save_callback`, `update_callback` and `destroy_callback` `:validate`, `:create`, `:save`, `:update` and `:destroy`
In your document inherits from `CouchRest::ExtendedDocument`, define your callback as follows: `CouchRest::CastedModel` instances have 1 callback already defined for you:
`:validate`
Define your callback as follows:
save_callback :before, :generate_slug_from_name set_callback :save, :before, :generate_slug_from_name
CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted from Rails 3, here are some simple usage examples: CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted from Rails 3, here are some simple usage examples:
save_callback :before, :before_method set_callback :save, :before, :before_method
save_callback :after, :after_method, :if => :condition set_callback :save, :after, :after_method, :if => :condition
save_callback :around {|r| stuff; yield; stuff } set_callback :save, :around {|r| stuff; yield; stuff }
Or the aliased short version:
before_save :before_method, :another_method
after_save :after_method, :another_method, :if => :condition
around_save {|r| stuff; yield; stuff }
To halt the callback, simply return a :halt symbol in your callback method.
Check the mixin or the ExtendedDocument class to see how to implement your own callbacks. Check the mixin or the ExtendedDocument class to see how to implement your own callbacks.
@ -162,4 +173,4 @@ Low level usage:
CouchRest is compatible with rails and can even be used a Rails plugin. CouchRest is compatible with rails and can even be used a Rails plugin.
However, you might be interested in the CouchRest companion rails project: However, you might be interested in the CouchRest companion rails project:
[http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails) [http://github.com/hpoydar/couchrest-rails](http://github.com/hpoydar/couchrest-rails)

View file

@ -2,7 +2,7 @@
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = %q{couchrest} s.name = %q{couchrest}
s.version = "0.33" s.version = "0.34"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["J. Chris Anderson", "Matt Aimonetti"] s.authors = ["J. Chris Anderson", "Matt Aimonetti"]

View file

@ -48,6 +48,7 @@ module CouchRest
require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'rest_api') require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'rest_api')
require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction') require File.join(File.dirname(__FILE__), 'couchrest', 'core', 'http_abstraction')
require File.join(File.dirname(__FILE__), 'couchrest', 'mixins') require File.join(File.dirname(__FILE__), 'couchrest', 'mixins')
require File.join(File.dirname(__FILE__), 'couchrest', 'support', 'rails') if defined?(Rails)
# we extend CouchRest with the RestAPI module which gives us acess to # we extend CouchRest with the RestAPI module which gives us acess to
# the get, post, put, delete and copy # the get, post, put, delete and copy

View file

@ -23,9 +23,10 @@ module CouchRest
end end
# returns true if the document has never been saved # returns true if the document has never been saved
def new_document? def new?
!rev !rev
end end
alias :new_document? :new?
# Saves the document to the db using create or update. Also runs the :save # Saves the document to the db using create or update. Also runs the :save
# callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on # callbacks. Sets the <tt>_id</tt> and <tt>_rev</tt> fields based on
@ -63,7 +64,7 @@ module CouchRest
# Returns the CouchDB uri for the document # Returns the CouchDB uri for the document
def uri(append_rev = false) def uri(append_rev = false)
return nil if new_document? return nil if new?
couch_uri = "http://#{database.root}/#{CGI.escape(id)}" couch_uri = "http://#{database.root}/#{CGI.escape(id)}"
if append_rev == true if append_rev == true
couch_uri << "?rev=#{rev}" couch_uri << "?rev=#{rev}"

View file

@ -1,8 +1,29 @@
require File.join(File.dirname(__FILE__), '..', 'support', 'class') # Copyright (c) 2006-2009 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
# http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
# http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
# Extracted from ActiveSupport::Callbacks written by Yehuda Katz require File.join(File.dirname(__FILE__), '..', 'support', 'class')
# http://github.com/wycats/rails/raw/abstract_controller/activesupport/lib/active_support/new_callbacks.rb
# http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb
module CouchRest module CouchRest
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
@ -85,19 +106,18 @@ module CouchRest
def self.included(klass) def self.included(klass)
klass.extend ClassMethods klass.extend ClassMethods
end end
def run_callbacks(kind, options = {}, &blk) def run_callbacks(kind, options = {}, &blk)
send("_run_#{kind}_callbacks", &blk) send("_run_#{kind}_callbacks", &blk)
end end
class Callback class Callback
@@_callback_sequence = 0 @@_callback_sequence = 0
attr_accessor :filter, :kind, :name, :options, :per_key, :klass attr_accessor :filter, :kind, :name, :options, :per_key, :klass
def initialize(filter, kind, options, klass, name) def initialize(filter, kind, options, klass)
@kind, @klass = kind, klass @kind, @klass = kind, klass
@name = name
normalize_options!(options) normalize_options!(options)
@per_key = options.delete(:per_key) @per_key = options.delete(:per_key)
@ -108,7 +128,7 @@ module CouchRest
_compile_per_key_options _compile_per_key_options
end end
def clone(klass) def clone(klass)
obj = super() obj = super()
obj.klass = klass obj.klass = klass
@ -120,23 +140,22 @@ module CouchRest
obj.options[:unless] = @options[:unless].dup obj.options[:unless] = @options[:unless].dup
obj obj
end end
def normalize_options!(options) def normalize_options!(options)
options[:if] = Array(options[:if]) options[:if] = Array.wrap(options[:if])
options[:unless] = Array(options[:unless]) options[:unless] = Array.wrap(options[:unless])
options[:per_key] ||= {} options[:per_key] ||= {}
options[:per_key][:if] = Array(options[:per_key][:if]) options[:per_key][:if] = Array.wrap(options[:per_key][:if])
options[:per_key][:unless] = Array(options[:per_key][:unless]) options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
end end
def next_id def next_id
@@_callback_sequence += 1 @@_callback_sequence += 1
end end
def matches?(_kind, _name, _filter) def matches?(_kind, _filter)
@kind == _kind && @kind == _kind &&
@name == _name &&
@filter == _filter @filter == _filter
end end
@ -144,11 +163,11 @@ module CouchRest
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless) filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if) filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
end end
def recompile!(_options, _per_key) def recompile!(_options, _per_key)
_update_filter(self.options, _options) _update_filter(self.options, _options)
_update_filter(self.per_key, _per_key) _update_filter(self.per_key, _per_key)
@callback_id = next_id @callback_id = next_id
@filter = _compile_filter(@raw_filter) @filter = _compile_filter(@raw_filter)
@compiled_options = _compile_options(@options) @compiled_options = _compile_options(@options)
@ -164,19 +183,19 @@ module CouchRest
end end
RUBY_EVAL RUBY_EVAL
end end
# This will supply contents for before and around filters, and no # This will supply contents for before and around filters, and no
# contents for after filters (for the forward pass). # contents for after filters (for the forward pass).
def start(key = nil, options = {}) def start(key = nil, options = {})
object, terminator = (options || {}).values_at(:object, :terminator) object, terminator = (options || {}).values_at(:object, :terminator)
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
terminator ||= false terminator ||= false
# options[0] is the compiled form of supplied conditions # options[0] is the compiled form of supplied conditions
# options[1] is the "end" for the conditional # options[1] is the "end" for the conditional
if @kind == :before || @kind == :around if @kind == :before || @kind == :around
if @kind == :before if @kind == :before
# if condition # before_save :filter_name, :if => :condition # if condition # before_save :filter_name, :if => :condition
@ -185,9 +204,10 @@ module CouchRest
filter = <<-RUBY_EVAL filter = <<-RUBY_EVAL
unless halted unless halted
result = #{@filter} result = #{@filter}
halted ||= (#{terminator}) halted = (#{terminator})
end end
RUBY_EVAL RUBY_EVAL
[@compiled_options[0], filter, @compiled_options[1]].compact.join("\n") [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n")
else else
# Compile around filters with conditions into proxy methods # Compile around filters with conditions into proxy methods
@ -204,9 +224,9 @@ module CouchRest
# yield self # yield self
# end # end
# end # end
name = "_conditional_callback_#{@kind}_#{next_id}" name = "_conditional_callback_#{@kind}_#{next_id}"
txt = <<-RUBY_EVAL txt, line = <<-RUBY_EVAL, __LINE__ + 1
def #{name}(halted) def #{name}(halted)
#{@compiled_options[0] || "if true"} && !halted #{@compiled_options[0] || "if true"} && !halted
#{@filter} do #{@filter} do
@ -217,19 +237,19 @@ module CouchRest
end end
end end
RUBY_EVAL RUBY_EVAL
@klass.class_eval(txt) @klass.class_eval(txt, __FILE__, line)
"#{name}(halted) do" "#{name}(halted) do"
end end
end end
end end
# This will supply contents for around and after filters, but not # This will supply contents for around and after filters, but not
# before filters (for the backward pass). # before filters (for the backward pass).
def end(key = nil, options = {}) def end(key = nil, options = {})
object = (options || {})[:object] object = (options || {})[:object]
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
if @kind == :around || @kind == :after if @kind == :around || @kind == :after
# if condition # after_save :filter_name, :if => :condition # if condition # after_save :filter_name, :if => :condition
# filter_name # filter_name
@ -241,27 +261,27 @@ module CouchRest
end end
end end
end end
private private
# Options support the same options as filters themselves (and support # Options support the same options as filters themselves (and support
# symbols, string, procs, and objects), so compile a conditional # symbols, string, procs, and objects), so compile a conditional
# expression based on the options # expression based on the options
def _compile_options(options) def _compile_options(options)
return [] if options[:if].empty? && options[:unless].empty? return [] if options[:if].empty? && options[:unless].empty?
conditions = [] conditions = []
unless options[:if].empty? unless options[:if].empty?
conditions << Array(_compile_filter(options[:if])) conditions << Array.wrap(_compile_filter(options[:if]))
end end
unless options[:unless].empty? unless options[:unless].empty?
conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
end end
["if #{conditions.flatten.join(" && ")}", "end"] ["if #{conditions.flatten.join(" && ")}", "end"]
end end
# Filters support: # Filters support:
# Arrays:: Used in conditions. This is used to specify # Arrays:: Used in conditions. This is used to specify
# multiple conditions. Used internally to # multiple conditions. Used internally to
@ -287,63 +307,72 @@ module CouchRest
filter.map {|f| _compile_filter(f)} filter.map {|f| _compile_filter(f)}
when Symbol when Symbol
filter filter
when String
"(#{filter})"
when Proc when Proc
@klass.send(:define_method, method_name, &filter) @klass.send(:define_method, method_name, &filter)
method_name << (filter.arity == 1 ? "(self)" : "") return method_name if filter.arity == 0
when String
@klass.class_eval <<-RUBY_EVAL method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
def #{method_name} else
#{filter} @klass.send(:define_method, "#{method_name}_object") { filter }
_normalize_legacy_filter(kind, filter)
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method_name}(&blk)
#{method_name}_object.send(:#{kind}, self, &blk)
end end
RUBY_EVAL RUBY_EVAL
method_name
else
kind, name = @kind, @name
@klass.send(:define_method, method_name) do
filter.send("#{kind}_#{name}", self)
end
method_name method_name
end end
end end
def _normalize_legacy_filter(kind, filter)
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
filter.class_eval(
"def #{kind}(context, &block) filter(context, &block) end",
__FILE__, __LINE__ - 1)
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
def filter.around(context)
should_continue = before(context)
yield if should_continue
after(context)
end
end
end
end end
# This method_missing is supplied to catch callbacks with keys and create
# the appropriate callback for future use.
def method_missing(meth, *args, &blk)
if meth.to_s =~ /_run__([\w:]+)__(\w+)__(\w+)__callbacks/
return self.class._create_and_run_keyed_callback($1, $2.to_sym, $3.to_sym, self, &blk)
end
super
end
# An Array with a compile method # An Array with a compile method
class CallbackChain < Array class CallbackChain < Array
def initialize(symbol) def initialize(symbol)
@symbol = symbol @symbol = symbol
end end
def compile(key = nil, options = {}) def compile(key = nil, options = {})
method = [] method = []
method << "halted = false" method << "halted = false"
each do |callback| each do |callback|
method << callback.start(key, options) method << callback.start(key, options)
end end
method << "yield self if block_given?" method << "yield self if block_given? && !halted"
reverse_each do |callback| reverse_each do |callback|
method << callback.end(key, options) method << callback.end(key, options)
end end
method.compact.join("\n") method.compact.join("\n")
end end
def clone(klass) def clone(klass)
chain = CallbackChain.new(@symbol) chain = CallbackChain.new(@symbol)
chain.push(*map {|c| c.clone(klass)}) chain.push(*map {|c| c.clone(klass)})
end end
end end
module ClassMethods module ClassMethods
CHAINS = {:before => :before, :around => :before, :after => :after} unless self.const_defined?("CHAINS") #CHAINS = {:before => :before, :around => :before, :after => :after}
# Make the _run_save_callbacks method. The generated method takes # Make the _run_save_callbacks method. The generated method takes
# a block that it'll yield to. It'll call the before and around filters # a block that it'll yield to. It'll call the before and around filters
# in order, yield the block, and then run the after filters. # in order, yield the block, and then run the after filters.
@ -355,43 +384,45 @@ module CouchRest
# The _run_save_callbacks method can optionally take a key, which # The _run_save_callbacks method can optionally take a key, which
# will be used to compile an optimized callback method for each # will be used to compile an optimized callback method for each
# key. See #define_callbacks for more information. # key. See #define_callbacks for more information.
def _define_runner(symbol, str, options) def _define_runner(symbol)
str = <<-RUBY_EVAL body = send("_#{symbol}_callback").
def _run_#{symbol}_callbacks(key = nil) compile(nil, :terminator => send("_#{symbol}_terminator"))
body, line = <<-RUBY_EVAL, __LINE__ + 1
def _run_#{symbol}_callbacks(key = nil, &blk)
if key if key
send("_run__\#{self.class.name.split("::").last}__#{symbol}__\#{key}__callbacks") { yield if block_given? } name = "_run__\#{self.class.name.hash.abs}__#{symbol}__\#{key.hash.abs}__callbacks"
unless respond_to?(name)
self.class._create_keyed_callback(name, :#{symbol}, self, &blk)
end
send(name, &blk)
else else
#{str} #{body}
end end
end end
RUBY_EVAL RUBY_EVAL
class_eval str, __FILE__, __LINE__ + 1 undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks")
class_eval body, __FILE__, line
before_name, around_name, after_name =
options.values_at(:before, :after, :around)
end end
# This is called the first time a callback is called with a particular # This is called the first time a callback is called with a particular
# key. It creates a new callback method for the key, calculating # key. It creates a new callback method for the key, calculating
# which callbacks can be omitted because of per_key conditions. # which callbacks can be omitted because of per_key conditions.
def _create_and_run_keyed_callback(klass, kind, key, obj, &blk) def _create_keyed_callback(name, kind, obj, &blk)
@_keyed_callbacks ||= {} @_keyed_callbacks ||= {}
@_keyed_callbacks[[kind, key]] ||= begin @_keyed_callbacks[name] ||= begin
str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator")) str = send("_#{kind}_callback").
compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
class_eval "def #{name}() #{str} end", __FILE__, __LINE__
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def _run__#{klass.split("::").last}__#{kind}__#{key}__callbacks
#{str}
end
RUBY_EVAL
true true
end end
obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk)
end end
# Define callbacks. # Define callbacks.
# #
# Creates a <name>_callback method that you can use to add callbacks. # Creates a <name>_callback method that you can use to add callbacks.
@ -423,59 +454,77 @@ module CouchRest
# In that case, each action_name would get its own compiled callback # In that case, each action_name would get its own compiled callback
# method that took into consideration the per_key conditions. This # method that took into consideration the per_key conditions. This
# is a speed improvement for ActionPack. # is a speed improvement for ActionPack.
def _update_callbacks(name, filters = CallbackChain.new(name), block = nil)
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(block) if block
callbacks = send("_#{name}_callback")
yield callbacks, type, filters, options if block_given?
_define_runner(name)
end
alias_method :_reset_callbacks, :_update_callbacks
def set_callback(name, *filters, &block)
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
filters.map! do |filter|
# overrides parent class
callbacks.delete_if {|c| c.matches?(type, filter) }
Callback.new(filter, type, options.dup, self)
end
options[:prepend] ? callbacks.unshift(*filters) : callbacks.push(*filters)
end
end
def skip_callback(name, *filters, &block)
_update_callbacks(name, filters, block) do |callbacks, type, filters, options|
filters.each do |filter|
callbacks = send("_#{name}_callback=", callbacks.clone(self))
filter = callbacks.find {|c| c.matches?(type, filter) }
if filter && options.any?
filter.recompile!(options, options[:per_key] || {})
else
callbacks.delete(filter)
end
end
end
end
def define_callbacks(*symbols) def define_callbacks(*symbols)
terminator = symbols.pop if symbols.last.is_a?(String) terminator = symbols.pop if symbols.last.is_a?(String)
symbols.each do |symbol| symbols.each do |symbol|
self.extlib_inheritable_accessor("_#{symbol}_terminator") extlib_inheritable_accessor("_#{symbol}_terminator") { terminator }
self.send("_#{symbol}_terminator=", terminator)
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
extlib_inheritable_accessor :_#{symbol}_callbacks
self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
def self.#{symbol}_callback(*filters, &blk) extlib_inheritable_accessor("_#{symbol}_callback") do
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before CallbackChain.new(symbol)
options = filters.last.is_a?(Hash) ? filters.pop : {} end
filters.unshift(blk) if block_given?
_define_runner(symbol)
filters.map! do |filter|
# overrides parent class # Define more convenient callback methods
self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)} # set_callback(:save, :before) becomes before_save
Callback.new(filter, type, options.dup, self, :#{symbol}) [:before, :after, :around].each do |filter|
end self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
self._#{symbol}_callbacks.push(*filters) def self.#{filter}_#{symbol}(*symbols, &blk)
_define_runner(:#{symbol}, _alias_callbacks(symbols, blk) do |callback, options|
self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator), set_callback(:#{symbol}, :#{filter}, callback, options)
options)
end
def self.skip_#{symbol}_callback(*filters, &blk)
type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
options = filters.last.is_a?(Hash) ? filters.pop : {}
filters.unshift(blk) if block_given?
filters.each do |filter|
self._#{symbol}_callbacks = self._#{symbol}_callbacks.clone(self)
filter = self._#{symbol}_callbacks.find {|c| c.matches?(type, :#{symbol}, filter) }
per_key = options[:per_key] || {}
if filter
filter.recompile!(options, per_key)
else
self._#{symbol}_callbacks.delete(filter)
end end
_define_runner(:#{symbol},
self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
options)
end end
RUBY_EVAL
end end
end
def self.reset_#{symbol}_callbacks end
self._#{symbol}_callbacks = CallbackChain.new(:#{symbol})
_define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {}) def _alias_callbacks(callbacks, block)
end options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
callbacks.push(block) if block
self.#{symbol}_callback(:before) callbacks.each do |callback|
RUBY_EVAL yield callback, options
end end
end end
end end

View file

@ -27,7 +27,7 @@ module CouchRest
class IncludeError < StandardError; end class IncludeError < StandardError; end
def self.included(base) def self.included(base)
base.class_eval <<-EOS, __FILE__, __LINE__ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties) extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
self.properties ||= [] self.properties ||= []
EOS EOS
@ -36,7 +36,7 @@ module CouchRest
end end
def apply_defaults def apply_defaults
return if self.respond_to?(:new_document?) && (new_document? == false) return if self.respond_to?(:new?) && (new? == false)
return unless self.class.respond_to?(:properties) return unless self.class.respond_to?(:properties)
return if self.class.properties.empty? return if self.class.properties.empty?
# TODO: cache the default object # TODO: cache the default object
@ -56,50 +56,76 @@ module CouchRest
def cast_keys def cast_keys
return unless self.class.properties return unless self.class.properties
self.class.properties.each do |property| self.class.properties.each do |property|
next unless property.casted cast_property(property)
key = self.has_key?(property.name) ? property.name : property.name.to_sym
# Don't cast the property unless it has a value
next unless self[key]
target = property.type
if target.is_a?(Array)
klass = ::CouchRest.constantize(target[0])
self[property.name] = self[key].collect do |value|
# Auto parse Time objects
obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)
obj.casted_by = self if obj.respond_to?(:casted_by)
obj
end
else
# Auto parse Time objects
self[property.name] = if ((property.init_method == 'new') && target == 'Time')
# Using custom time parsing method because Ruby's default method is toooo slow
self[key].is_a?(String) ? Time.mktime_with_offset(self[key].dup) : self[key]
# Float instances don't get initialized with #new
elsif ((property.init_method == 'new') && target == 'Float')
cast_float(self[key])
# 'boolean' type is simply used to generate a property? accessor method
elsif ((property.init_method == 'new') && target == 'boolean')
self[key]
else
# Let people use :send as a Time parse arg
klass = ::CouchRest.constantize(target)
klass.send(property.init_method, self[key].dup)
end
self[property.name].casted_by = self if self[property.name].respond_to?(:casted_by)
end
end end
end
def cast_float(value)
begin def cast_property(property, assigned=false)
Float(value) return unless property.casted
rescue key = self.has_key?(property.name) ? property.name : property.name.to_sym
# Don't cast the property unless it has a value
return unless self[key]
if property.type.is_a?(Array)
klass = ::CouchRest.constantize(property.type[0])
arr = self[key].dup.collect do |value|
unless value.instance_of?(klass)
value = convert_property_value(property, klass, value)
end
associate_casted_to_parent(value, assigned)
value value
end end
self[key] = klass != String ? CastedArray.new(arr) : arr
self[key].casted_by = self if self[key].respond_to?(:casted_by)
else
if property.type == 'boolean'
klass = TrueClass
else
klass = ::CouchRest.constantize(property.type)
end
unless self[key].instance_of?(klass)
self[key] = convert_property_value(property, klass, self[property.name])
end
associate_casted_to_parent(self[property.name], assigned)
end end
end end
def associate_casted_to_parent(casted, assigned)
casted.casted_by = self if casted.respond_to?(:casted_by)
casted.document_saved = true if !assigned && casted.respond_to?(:document_saved)
end
def convert_property_value(property, klass, value)
if ((property.init_method == 'new') && klass == Time)
# Using custom time parsing method because Ruby's default method is toooo slow
value.is_a?(String) ? Time.mktime_with_offset(value.dup) : value
# Float instances don't get initialized with #new
elsif ((property.init_method == 'new') && klass == Float)
cast_float(value)
# 'boolean' type is simply used to generate a property? accessor method
elsif ((property.init_method == 'new') && klass == TrueClass)
value
else
klass.send(property.init_method, value.dup)
end
end
def cast_property_by_name(property_name)
return unless self.class.properties
property = self.class.properties.detect{|property| property.name == property_name}
return unless property
cast_property(property, true)
end
def cast_float(value)
begin
Float(value)
rescue
value
end
end
module ClassMethods module ClassMethods
def property(name, options={}) def property(name, options={})
@ -125,7 +151,7 @@ module CouchRest
# defines the getter for the property (and optional aliases) # defines the getter for the property (and optional aliases)
def create_property_getter(property) def create_property_getter(property)
# meth = property.name # meth = property.name
class_eval <<-EOS, __FILE__, __LINE__ class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{property.name} def #{property.name}
self['#{property.name}'] self['#{property.name}']
end end
@ -144,7 +170,7 @@ module CouchRest
end end
if property.alias if property.alias
class_eval <<-EOS, __FILE__, __LINE__ class_eval <<-EOS, __FILE__, __LINE__ + 1
alias #{property.alias.to_sym} #{property.name.to_sym} alias #{property.alias.to_sym} #{property.name.to_sym}
EOS EOS
end end
@ -152,16 +178,17 @@ module CouchRest
# defines the setter for the property (and optional aliases) # defines the setter for the property (and optional aliases)
def create_property_setter(property) def create_property_setter(property)
meth = property.name property_name = property.name
class_eval <<-EOS class_eval <<-EOS
def #{meth}=(value) def #{property_name}=(value)
self['#{meth}'] = value self['#{property_name}'] = value
cast_property_by_name('#{property_name}')
end end
EOS EOS
if property.alias if property.alias
class_eval <<-EOS class_eval <<-EOS
alias #{property.alias.to_sym}= #{meth.to_sym}= alias #{property.alias.to_sym}= #{property_name.to_sym}=
EOS EOS
end end
end end

View file

@ -50,7 +50,10 @@ module CouchRest
def self.included(base) def self.included(base)
base.extlib_inheritable_accessor(:auto_validation) base.extlib_inheritable_accessor(:auto_validation)
base.class_eval <<-EOS, __FILE__, __LINE__ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
# Callbacks
define_callbacks :validate
# Turn off auto validation by default # Turn off auto validation by default
self.auto_validation ||= false self.auto_validation ||= false
@ -71,9 +74,10 @@ module CouchRest
EOS EOS
base.extend(ClassMethods) base.extend(ClassMethods)
base.class_eval <<-EOS, __FILE__, __LINE__ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
define_callbacks :validate
if method_defined?(:_run_save_callbacks) if method_defined?(:_run_save_callbacks)
save_callback :before, :check_validations set_callback :save, :before, :check_validations
end end
EOS EOS
base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
@ -115,8 +119,7 @@ module CouchRest
# Check if a resource is valid in a given context # Check if a resource is valid in a given context
# #
def valid?(context = :default) def valid?(context = :default)
result = self.class.validators.execute(context, self) recursive_valid?(self, context, true)
result && validate_casted_arrays
end end
# checking on casted objects # checking on casted objects
@ -133,29 +136,24 @@ module CouchRest
result result
end end
# Begin a recursive walk of the model checking validity
#
def all_valid?(context = :default)
recursive_valid?(self, context, true)
end
# Do recursive validity checking # Do recursive validity checking
# #
def recursive_valid?(target, context, state) def recursive_valid?(target, context, state)
valid = state valid = state
target.instance_variables.each do |ivar| target.each do |key, prop|
ivar_value = target.instance_variable_get(ivar) if prop.is_a?(Array)
if ivar_value.validatable? prop.each do |item|
valid = valid && recursive_valid?(ivar_value, context, valid)
elsif ivar_value.respond_to?(:each)
ivar_value.each do |item|
if item.validatable? if item.validatable?
valid = valid && recursive_valid?(item, context, valid) valid = recursive_valid?(item, context, valid) && valid
end end
end end
elsif prop.validatable?
valid = recursive_valid?(prop, context, valid) && valid
end end
end end
return valid && target.valid? target._run_validate_callbacks do
target.class.validators.execute(context, target) && valid
end
end end
@ -212,21 +210,12 @@ module CouchRest
def create_context_instance_methods(context) def create_context_instance_methods(context)
name = "valid_for_#{context.to_s}?" # valid_for_signup? name = "valid_for_#{context.to_s}?" # valid_for_signup?
if !self.instance_methods.include?(name) if !self.instance_methods.include?(name)
class_eval <<-EOS, __FILE__, __LINE__ class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name} # def valid_for_signup? def #{name} # def valid_for_signup?
valid?('#{context.to_s}'.to_sym) # valid?('signup'.to_sym) valid?('#{context.to_s}'.to_sym) # valid?('signup'.to_sym)
end # end end # end
EOS EOS
end end
all = "all_valid_for_#{context.to_s}?" # all_valid_for_signup?
if !self.instance_methods.include?(all)
class_eval <<-EOS, __FILE__, __LINE__
def #{all} # def all_valid_for_signup?
all_valid?('#{context.to_s}'.to_sym) # all_valid?('signup'.to_sym)
end # end
EOS
end
end end
# Create a new validator of the given klazz and push it onto the # Create a new validator of the given klazz and push it onto the

View file

@ -5,8 +5,10 @@ module CouchRest
module CastedModel module CastedModel
def self.included(base) def self.included(base)
base.send(:include, ::CouchRest::Callbacks)
base.send(:include, ::CouchRest::Mixins::Properties) base.send(:include, ::CouchRest::Mixins::Properties)
base.send(:attr_accessor, :casted_by) base.send(:attr_accessor, :casted_by)
base.send(:attr_accessor, :document_saved)
end end
def initialize(keys={}) def initialize(keys={})
@ -26,5 +28,31 @@ module CouchRest
def [] key def [] key
super(key.to_s) super(key.to_s)
end 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?
!@document_saved
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
end end

View file

@ -21,7 +21,7 @@ module CouchRest
def self.inherited(subklass) def self.inherited(subklass)
subklass.send(:include, CouchRest::Mixins::Properties) subklass.send(:include, CouchRest::Mixins::Properties)
subklass.class_eval <<-EOS, __FILE__, __LINE__ subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass) def self.inherited(subklass)
subklass.properties = self.properties.dup subklass.properties = self.properties.dup
end end
@ -33,10 +33,10 @@ module CouchRest
attr_accessor :casted_by attr_accessor :casted_by
# Callbacks # Callbacks
define_callbacks :create define_callbacks :create, "result == :halt"
define_callbacks :save define_callbacks :save, "result == :halt"
define_callbacks :update define_callbacks :update, "result == :halt"
define_callbacks :destroy define_callbacks :destroy, "result == :halt"
def initialize(passed_keys={}) def initialize(passed_keys={})
apply_defaults # defined in CouchRest::Mixins::Properties apply_defaults # defined in CouchRest::Mixins::Properties
@ -76,17 +76,17 @@ module CouchRest
# on the document whenever saving occurs. CouchRest uses a pretty # on the document whenever saving occurs. CouchRest uses a pretty
# decent time format by default. See Time#to_json # decent time format by default. See Time#to_json
def self.timestamps! def self.timestamps!
class_eval <<-EOS, __FILE__, __LINE__ class_eval <<-EOS, __FILE__, __LINE__ + 1
property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false) property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
save_callback :before do |object| set_callback :save, :before do |object|
object['updated_at'] = Time.now object['updated_at'] = Time.now
object['created_at'] = object['updated_at'] if object.new_document? object['created_at'] = object['updated_at'] if object.new?
end end
EOS EOS
end end
# Name a method that will be called before the document is first saved, # 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>. # 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, # Because CouchDB enforces a constraint that each id must be unique,
@ -128,17 +128,36 @@ module CouchRest
self.class.properties self.class.properties
end end
# 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
# Takes a hash as argument, and applies the values by using writer methods # 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 # 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. # missing. In case of error, no attributes are changed.
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
hash.each do |k, v| # remove attributes that cannot be updated, silently ignoring them
# which matches Rails behavior when, for instance, setting created_at.
# make a copy, we don't want to change arguments
attrs = hash.dup
%w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
attrs.each do |k, v|
raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=") raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
end end
hash.each do |k, v| attrs.each do |k, v|
self.send("#{k}=",v) self.send("#{k}=",v)
end end
end end
alias :attributes= :update_attributes_without_saving
# Takes a hash as argument, and applies the values by using writer methods # Takes a hash as argument, and applies the values by using writer methods
# for each key. Raises a NoMethodError if the corresponding methods are # for each key. Raises a NoMethodError if the corresponding methods are
@ -217,7 +236,8 @@ module CouchRest
raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
set_unique_id if new_document? && self.respond_to?(:set_unique_id) set_unique_id if new_document? && self.respond_to?(:set_unique_id)
result = database.save_doc(self, bulk) result = database.save_doc(self, bulk)
return true mark_as_saved
true
end end
# Saves the document to the db using save. Raises an exception # Saves the document to the db using save. Raises an exception
@ -242,5 +262,22 @@ module CouchRest
end end
end end
protected
# Set document_saved flag on all casted models to true
def mark_as_saved
self.each do |key, prop|
if prop.is_a?(Array)
prop.each do |item|
if item.respond_to?(:document_saved)
item.send(:document_saved=, true)
end
end
elsif prop.respond_to?(:document_saved)
prop.send(:document_saved=, true)
end
end
end
end end
end end

View file

@ -38,3 +38,22 @@ module CouchRest
end end
end end
class CastedArray < Array
attr_accessor :casted_by
def << obj
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
super(obj)
end
def push(obj)
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
super(obj)
end
def []= index, obj
obj.casted_by = self.casted_by if obj.respond_to?(:casted_by)
super(index, obj)
end
end

View file

@ -1,5 +1,5 @@
# Copyright (c) 2004-2008 David Heinemeier Hansson # Copyright (c) 2006-2009 David Heinemeier Hansson
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the # a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including # "Software"), to deal in the Software without restriction, including
@ -7,10 +7,10 @@
# distribute, sublicense, and/or sell copies of the Software, and to # distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to # permit persons to whom the Software is furnished to do so, subject to
# the following conditions: # the following conditions:
# #
# The above copyright notice and this permission notice shall be # The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software. # included in all copies or substantial portions of the Software.
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
@ -18,79 +18,59 @@
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Allows attributes to be shared within an inheritance hierarchy, but where # Extracted From
# each descendant gets a copy of their parents' attributes, instead of just a # http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2
# pointer to the same. This means that the child can add elements to, for
# example, an array without those additions being shared with either their # Extends the class object with class and instance accessors for class attributes,
# parent, siblings, or children, which is unlike the regular class-level # just like the native attr* accessors for instance attributes.
# attributes that are shared across the entire hierarchy. #
# class Person
# cattr_accessor :hair_colors
# end
#
# Person.hair_colors = [:brown, :black, :blonde, :red]
class Class class Class
# Defines class-level and instance-level attribute reader.
#
# @param *syms<Array> Array of attributes to define reader for.
# @return <Array[#to_s]> List of attributes that were made into cattr_readers
#
# @api public
#
# @todo Is this inconsistent in that it does not allow you to prevent
# an instance_reader via :instance_reader => false
def cattr_reader(*syms) def cattr_reader(*syms)
syms.flatten.each do |sym| syms.flatten.each do |sym|
next if sym.is_a?(Hash) next if sym.is_a?(Hash)
class_eval(<<-RUBY, __FILE__, __LINE__ + 1) class_eval(<<-EOS, __FILE__, __LINE__ + 1)
unless defined? @@#{sym} unless defined? @@#{sym} # unless defined? @@hair_colors
@@#{sym} = nil @@#{sym} = nil # @@hair_colors = nil
end end # end
#
def self.#{sym} def self.#{sym} # def self.hair_colors
@@#{sym} @@#{sym} # @@hair_colors
end end # end
#
def #{sym} def #{sym} # def hair_colors
@@#{sym} @@#{sym} # @@hair_colors
end end # end
RUBY EOS
end end
end unless Class.respond_to?(:cattr_reader) end unless Class.respond_to?(:cattr_reader)
# Defines class-level (and optionally instance-level) attribute writer.
#
# @param <Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define writer for.
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
# @return <Array[#to_s]> List of attributes that were made into cattr_writers
#
# @api public
def cattr_writer(*syms) def cattr_writer(*syms)
options = syms.last.is_a?(Hash) ? syms.pop : {} options = syms.extract_options!
syms.flatten.each do |sym| syms.flatten.each do |sym|
class_eval(<<-RUBY, __FILE__, __LINE__ + 1) class_eval(<<-EOS, __FILE__, __LINE__ + 1)
unless defined? @@#{sym} unless defined? @@#{sym} # unless defined? @@hair_colors
@@#{sym} = nil @@#{sym} = nil # @@hair_colors = nil
end end # end
#
def self.#{sym}=(obj) def self.#{sym}=(obj) # def self.hair_colors=(obj)
@@#{sym} = obj @@#{sym} = obj # @@hair_colors = obj
end end # end
RUBY #
#{" #
unless options[:instance_writer] == false def #{sym}=(obj) # def hair_colors=(obj)
class_eval(<<-RUBY, __FILE__, __LINE__ + 1) @@#{sym} = obj # @@hair_colors = obj
def #{sym}=(obj) end # end
@@#{sym} = obj " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
end EOS
RUBY
end
end end
end unless Class.respond_to?(:cattr_writer) end unless Class.respond_to?(:cattr_writer)
# Defines class-level (and optionally instance-level) attribute accessor.
#
# @param *syms<Array[*#to_s, Hash{:instance_writer => Boolean}]> Array of attributes to define accessor for.
# @option syms :instance_writer<Boolean> if true, instance-level attribute writer is defined.
# @return <Array[#to_s]> List of attributes that were made into accessors
#
# @api public
def cattr_accessor(*syms) def cattr_accessor(*syms)
cattr_reader(*syms) cattr_reader(*syms)
cattr_writer(*syms) cattr_writer(*syms)
@ -156,6 +136,8 @@ class Class
def #{ivar}=(obj) self.class.#{ivar} = obj end def #{ivar}=(obj) self.class.#{ivar} = obj end
RUBY RUBY
end end
self.send("#{ivar}=", yield) if block_given?
end end
end unless Class.respond_to?(:extlib_inheritable_writer) end unless Class.respond_to?(:extlib_inheritable_writer)
@ -168,9 +150,41 @@ class Class
# @return <Array[#to_s]> An Array of attributes turned into inheritable accessors. # @return <Array[#to_s]> An Array of attributes turned into inheritable accessors.
# #
# @api public # @api public
def extlib_inheritable_accessor(*syms) def extlib_inheritable_accessor(*syms, &block)
extlib_inheritable_reader(*syms) extlib_inheritable_reader(*syms)
extlib_inheritable_writer(*syms) extlib_inheritable_writer(*syms, &block)
end unless Class.respond_to?(:extlib_inheritable_accessor) end unless Class.respond_to?(:extlib_inheritable_accessor)
end end
class Array
# Extracts options from a set of arguments. Removes and returns the last
# element in the array if it's a hash, otherwise returns a blank hash.
#
# def options(*args)
# args.extract_options!
# end
#
# options(1, 2) # => {}
# options(1, 2, :a => :b) # => {:a=>:b}
def extract_options!
last.is_a?(::Hash) ? pop : {}
end unless Array.new.respond_to?(:extract_options!)
# Wraps the object in an Array unless it's an Array. Converts the
# object to an Array using #to_ary if it implements that.
def self.wrap(object)
case object
when nil
[]
when self
object
else
if object.respond_to?(:to_ary)
object.to_ary
else
[object]
end
end
end unless Array.respond_to?(:wrap)
end

View file

@ -1,8 +1,4 @@
# This file contains various hacks for Rails compatibility. # This file contains various hacks for Rails compatibility.
# To use, just require in environment.rb, like so:
#
# require 'couchrest/support/rails'
class Hash class Hash
# Hack so that CouchRest::Document, which descends from Hash, # Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options # doesn't appear to Rails routing as a Hash of options
@ -12,8 +8,10 @@ class Hash
end end
end end
CouchRest::Document.class_eval do 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, # Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options # doesn't appear to Rails routing as a Hash of options
def is_a?(o) def is_a?(o)
@ -23,6 +21,15 @@ CouchRest::Document.class_eval do
alias_method :kind_of?, :is_a? alias_method :kind_of?, :is_a?
end end
CouchRest::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
require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors') require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors')

View file

@ -253,7 +253,7 @@ describe CouchRest::Database do
describe "PUT attachment from file" do describe "PUT attachment from file" do
before(:each) do before(:each) do
filename = FIXTURE_PATH + '/attachments/couchdb.png' filename = FIXTURE_PATH + '/attachments/couchdb.png'
@file = File.open(filename) @file = File.open(filename, "rb")
end end
after(:each) do after(:each) do
@file.close @file.close

View file

@ -43,16 +43,14 @@ describe "assigning a value to casted attribute after initializing an object" do
@car.driver.should be_nil @car.driver.should be_nil
end end
# Note that this isn't casting the attribute, it's just assigning it a value
# (see "should not cast attribute")
it "should let you assign the value" do it "should let you assign the value" do
@car.driver = @driver @car.driver = @driver
@car.driver.name.should == 'Matt' @car.driver.name.should == 'Matt'
end end
it "should not cast attribute" do it "should cast attribute" do
@car.driver = JSON.parse(JSON.generate(@driver)) @car.driver = JSON.parse(JSON.generate(@driver))
@car.driver.should_not be_instance_of(Driver) @car.driver.should be_instance_of(Driver)
end end
end end

View file

@ -4,6 +4,8 @@ require File.expand_path('../../../spec_helper', __FILE__)
require File.join(FIXTURE_PATH, 'more', 'card') require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'cat') require File.join(FIXTURE_PATH, 'more', 'cat')
require File.join(FIXTURE_PATH, 'more', 'person') require File.join(FIXTURE_PATH, 'more', 'person')
require File.join(FIXTURE_PATH, 'more', 'question')
require File.join(FIXTURE_PATH, 'more', 'course')
class WithCastedModelMixin < Hash class WithCastedModelMixin < Hash
@ -21,6 +23,26 @@ class DummyModel < CouchRest::ExtendedDocument
property :keywords, :cast_as => ["String"] property :keywords, :cast_as => ["String"]
end end
class CastedCallbackDoc < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :callback_model, :cast_as => 'WithCastedCallBackModel'
end
class WithCastedCallBackModel < Hash
include CouchRest::CastedModel
include CouchRest::Validation
property :name
property :run_before_validate
property :run_after_validate
before_validate do |object|
object.run_before_validate = true
end
after_validate do |object|
object.run_after_validate = true
end
end
describe CouchRest::CastedModel do describe CouchRest::CastedModel do
describe "A non hash class including CastedModel" do describe "A non hash class including CastedModel" do
@ -106,7 +128,40 @@ describe CouchRest::CastedModel do
@obj.keywords.should be_an_instance_of(Array) @obj.keywords.should be_an_instance_of(Array)
@obj.keywords.first.should == 'couch' @obj.keywords.first.should == 'couch'
end end
end
describe "update attributes without saving" do
before(:each) do
@question = Question.new(:q => "What is your quest?", :a => "To seek the Holy Grail")
end
it "should work for attribute= methods" do
@question.q.should == "What is your quest?"
@question['a'].should == "To seek the Holy Grail"
@question.update_attributes_without_saving(:q => "What is your favorite color?", 'a' => "Blue")
@question['q'].should == "What is your favorite color?"
@question.a.should == "Blue"
end
it "should also work for attributes= alias" do
@question.respond_to?(:attributes=).should be_true
@question.attributes = {:q => "What is your favorite color?", 'a' => "Blue"}
@question['q'].should == "What is your favorite color?"
@question.a.should == "Blue"
end
it "should flip out if an attribute= method is missing" do
lambda {
@q.update_attributes_without_saving('foo' => "something", :a => "No green")
}.should raise_error(NoMethodError)
end
it "should not change any attributes if there is an error" do
lambda {
@q.update_attributes_without_saving('foo' => "something", :a => "No green")
}.should raise_error(NoMethodError)
@question.q.should == "What is your quest?"
@question.a.should == "To seek the Holy Grail"
end
end end
describe "saved document with casted models" do describe "saved document with casted models" do
@ -154,6 +209,10 @@ describe CouchRest::CastedModel do
toy = CatToy.new :name => "Mouse" toy = CatToy.new :name => "Mouse"
@cat.toys.push(toy) @cat.toys.push(toy)
@cat.save.should be_true @cat.save.should be_true
@cat = Cat.get @cat.id
@cat.toys.class.should == CastedArray
@cat.toys.first.class.should == CatToy
@cat.toys.first.should === toy
end end
it "should fail because name is not present" do it "should fail because name is not present" do
@ -171,7 +230,177 @@ describe CouchRest::CastedModel do
cat.masters.push Person.new cat.masters.push Person.new
cat.should be_valid cat.should be_valid
end end
end end
describe "calling valid?" do
before :each do
@cat = Cat.new
@toy1 = CatToy.new
@toy2 = CatToy.new
@toy3 = CatToy.new
@cat.favorite_toy = @toy1
@cat.toys << @toy2
@cat.toys << @toy3
end
describe "on the top document" do
it "should put errors on all invalid casted models" do
@cat.should_not be_valid
@cat.errors.should_not be_empty
@toy1.errors.should_not be_empty
@toy2.errors.should_not be_empty
@toy3.errors.should_not be_empty
end
it "should not put errors on valid casted models" do
@toy1.name = "Feather"
@toy2.name = "Twine"
@cat.should_not be_valid
@cat.errors.should_not be_empty
@toy1.errors.should be_empty
@toy2.errors.should be_empty
@toy3.errors.should_not be_empty
end
end
describe "on a casted model property" do
it "should only validate itself" do
@toy1.should_not be_valid
@toy1.errors.should_not be_empty
@cat.errors.should be_empty
@toy2.errors.should be_empty
@toy3.errors.should be_empty
end
end
describe "on a casted model inside a casted collection" do
it "should only validate itself" do
@toy2.should_not be_valid
@toy2.errors.should_not be_empty
@cat.errors.should be_empty
@toy1.errors.should be_empty
@toy3.errors.should be_empty
end
end
end
describe "calling new? on a casted model" do
before :each do
reset_test_db!
@cat = Cat.new(:name => 'Sockington')
@favorite_toy = CatToy.new(:name => 'Catnip Ball')
@cat.favorite_toy = @favorite_toy
@cat.toys << CatToy.new(:name => 'Fuzzy Stick')
end
it "should be true on new" do
CatToy.new.should be_new
CatToy.new.new_record?.should be_true
end
it "should be true after assignment" do
@cat.should be_new
@cat.favorite_toy.should be_new
@cat.toys.first.should be_new
end
it "should not be true after create or save" do
@cat.create
@cat.save
@cat.favorite_toy.should_not be_new
@cat.toys.first.should_not be_new
end
it "should not be true after get from the database" do
@cat.save
@cat = Cat.get(@cat.id)
@cat.favorite_toy.should_not be_new
@cat.toys.first.should_not be_new
end
it "should still be true after a failed create or save" do
@cat.name = nil
@cat.create.should be_false
@cat.save.should be_false
@cat.favorite_toy.should be_new
@cat.toys.first.should be_new
end
end
describe "calling base_doc from a nested casted model" do
before :each do
@course = Course.new(:title => 'Science 101')
@professor = Person.new(:name => 'Professor Plum')
@cat = Cat.new(:name => 'Scratchy')
@toy1 = CatToy.new
@toy2 = CatToy.new
@course.professor = @professor
@professor.pet = @cat
@cat.favorite_toy = @toy1
@cat.toys << @toy2
end
it "should reference the top document for" do
@course.base_doc.should === @course
@professor.casted_by.should === @course
@professor.base_doc.should === @course
@cat.base_doc.should === @course
@toy1.base_doc.should === @course
@toy2.base_doc.should === @course
end
it "should call setter on top document" do
@toy1.base_doc.should_not be_nil
@toy1.base_doc.title = 'Tom Foolery'
@course.title.should == 'Tom Foolery'
end
it "should return nil if not yet casted" do
person = Person.new
person.base_doc.should == nil
end
end
describe "calling base_doc.save from a nested casted model" do
before :each do
reset_test_db!
@cat = Cat.new(:name => 'Snowball')
@toy = CatToy.new
@cat.favorite_toy = @toy
end
it "should not save parent document when casted model is invalid" do
@toy.should_not be_valid
@toy.base_doc.save.should be_false
lambda{@toy.base_doc.save!}.should raise_error
end
it "should save parent document when nested casted model is valid" do
@toy.name = "Mr Squeaks"
@toy.should be_valid
@toy.base_doc.save.should be_true
lambda{@toy.base_doc.save!}.should_not raise_error
end
end
describe "callbacks" do
before(:each) do
@doc = CastedCallbackDoc.new
@model = WithCastedCallBackModel.new
@doc.callback_model = @model
end
describe "validate" do
it "should run before_validate before validating" do
@model.run_before_validate.should be_nil
@model.should be_valid
@model.run_before_validate.should be_true
end
it "should run after_validate after validating" do
@model.run_after_validate.should be_nil
@model.should be_valid
@model.run_after_validate.should be_true
end
end
end
end end

View file

@ -1,6 +1,7 @@
require File.expand_path("../../../spec_helper", __FILE__) require File.expand_path("../../../spec_helper", __FILE__)
require File.join(FIXTURE_PATH, 'more', 'article') require File.join(FIXTURE_PATH, 'more', 'article')
require File.join(FIXTURE_PATH, 'more', 'course') require File.join(FIXTURE_PATH, 'more', 'course')
require File.join(FIXTURE_PATH, 'more', 'cat')
describe "ExtendedDocument" do describe "ExtendedDocument" do
@ -17,8 +18,11 @@ describe "ExtendedDocument" do
end end
class WithCallBacks < CouchRest::ExtendedDocument class WithCallBacks < CouchRest::ExtendedDocument
include ::CouchRest::Validation
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database
property :name property :name
property :run_before_validate
property :run_after_validate
property :run_before_save property :run_before_save
property :run_after_save property :run_after_save
property :run_before_create property :run_before_create
@ -26,24 +30,56 @@ describe "ExtendedDocument" do
property :run_before_update property :run_before_update
property :run_after_update property :run_after_update
save_callback :before do |object| before_validate do |object|
object.run_before_validate = true
end
after_validate do |object|
object.run_after_validate = true
end
before_save do |object|
object.run_before_save = true object.run_before_save = true
end end
save_callback :after do |object| after_save do |object|
object.run_after_save = true object.run_after_save = true
end end
create_callback :before do |object| before_create do |object|
object.run_before_create = true object.run_before_create = true
end end
create_callback :after do |object| after_create do |object|
object.run_after_create = true object.run_after_create = true
end end
update_callback :before do |object| before_update do |object|
object.run_before_update = true object.run_before_update = true
end end
update_callback :after do |object| after_update do |object|
object.run_after_update = true object.run_after_update = true
end end
property :run_one
property :run_two
property :run_three
before_save :run_one_method, :run_two_method do |object|
object.run_three = true
end
def run_one_method
self.run_one = true
end
def run_two_method
self.run_two = true
end
attr_accessor :run_it
property :conditional_one
property :conditional_two
before_save :conditional_one_method, :conditional_two_method, :if => proc { self.run_it }
def conditional_one_method
self.conditional_one = true
end
def conditional_two_method
self.conditional_two = true
end
end end
class WithTemplateAndUniqueID < CouchRest::ExtendedDocument class WithTemplateAndUniqueID < CouchRest::ExtendedDocument
@ -85,15 +121,12 @@ describe "ExtendedDocument" do
end end
describe "a new model" do describe "a new model" do
it "should be a new_record" do it "should be a new document" do
@obj = Basic.new @obj = Basic.new
@obj.rev.should be_nil @obj.rev.should be_nil
@obj.should be_a_new_record @obj.should be_new
end @obj.should be_new_document
it "should be a new_document" do @obj.should be_new_record
@obj = Basic.new
@obj.rev.should be_nil
@obj.should be_a_new_document
end end
end end
@ -101,7 +134,7 @@ describe "ExtendedDocument" do
it "should instantialize and save a document" do it "should instantialize and save a document" do
article = Article.create(:title => 'my test') article = Article.create(:title => 'my test')
article.title.should == 'my test' article.title.should == 'my test'
article.should_not be_new_document article.should_not be_new
end end
it "should trigger the create callbacks" do it "should trigger the create callbacks" do
@ -125,6 +158,27 @@ describe "ExtendedDocument" do
@art.update_attributes_without_saving('date' => Time.now, :title => "super danger") @art.update_attributes_without_saving('date' => Time.now, :title => "super danger")
@art['title'].should == "super danger" @art['title'].should == "super danger"
end end
it "should silently ignore _id" do
@art.update_attributes_without_saving('_id' => 'foobar')
@art['_id'].should_not == 'foobar'
end
it "should silently ignore _rev" do
@art.update_attributes_without_saving('_rev' => 'foobar')
@art['_rev'].should_not == 'foobar'
end
it "should silently ignore created_at" do
@art.update_attributes_without_saving('created_at' => 'foobar')
@art['created_at'].should_not == 'foobar'
end
it "should silently ignore updated_at" do
@art.update_attributes_without_saving('updated_at' => 'foobar')
@art['updated_at'].should_not == 'foobar'
end
it "should also work using attributes= alias" do
@art.respond_to?(:attributes=).should be_true
@art.attributes = {'date' => Time.now, :title => "something else"}
@art['title'].should == "something else"
end
it "should flip out if an attribute= method is missing" do it "should flip out if an attribute= method is missing" do
lambda { lambda {
@ -419,7 +473,7 @@ describe "ExtendedDocument" do
end end
it "should be a new document" do it "should be a new document" do
@art.should be_a_new_document @art.should be_new
@art.title.should be_nil @art.title.should be_nil
end end
@ -527,12 +581,46 @@ describe "ExtendedDocument" do
@doc = WithCallBacks.new @doc = WithCallBacks.new
end end
describe "validate" do
it "should run before_validate before validating" do
@doc.run_before_validate.should be_nil
@doc.should be_valid
@doc.run_before_validate.should be_true
end
it "should run after_validate after validating" do
@doc.run_after_validate.should be_nil
@doc.should be_valid
@doc.run_after_validate.should be_true
end
end
describe "save" do describe "save" do
it "should run the after filter after saving" do it "should run the after filter after saving" do
@doc.run_after_save.should be_nil @doc.run_after_save.should be_nil
@doc.save.should be_true @doc.save.should be_true
@doc.run_after_save.should be_true @doc.run_after_save.should be_true
end end
it "should run the grouped callbacks before saving" do
@doc.run_one.should be_nil
@doc.run_two.should be_nil
@doc.run_three.should be_nil
@doc.save.should be_true
@doc.run_one.should be_true
@doc.run_two.should be_true
@doc.run_three.should be_true
end
it "should not run conditional callbacks" do
@doc.run_it = false
@doc.save.should be_true
@doc.conditional_one.should be_nil
@doc.conditional_two.should be_nil
end
it "should run conditional callbacks" do
@doc.run_it = true
@doc.save.should be_true
@doc.conditional_one.should be_true
@doc.conditional_two.should be_true
end
end end
describe "create" do describe "create" do
it "should run the before save filter when creating" do it "should run the before save filter when creating" do
@ -585,4 +673,49 @@ describe "ExtendedDocument" do
@doc.other_arg.should == "foo-foo" @doc.other_arg.should == "foo-foo"
end end
end end
describe "recursive validation on an extended document" do
before :each do
reset_test_db!
@cat = Cat.new(:name => 'Sockington')
end
it "should not save if a nested casted model is invalid" do
@cat.favorite_toy = CatToy.new
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested casted model is valid" do
@cat.favorite_toy = CatToy.new(:name => 'Squeaky')
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not save when nested collection contains an invalid casted model" do
@cat.toys = [CatToy.new(:name => 'Feather'), CatToy.new]
@cat.should_not be_valid
@cat.save.should be_false
lambda{@cat.save!}.should raise_error
end
it "should save when nested collection contains valid casted models" do
@cat.toys = [CatToy.new(:name => 'feather'), CatToy.new(:name => 'ball-o-twine')]
@cat.should be_valid
@cat.save.should be_true
lambda{@cat.save!}.should_not raise_error
end
it "should not fail if the nested casted model doesn't have validation" do
Cat.property :trainer, :cast_as => 'Person'
Cat.validates_present :name
cat = Cat.new(:name => 'Mr Bigglesworth')
cat.trainer = Person.new
cat.trainer.validatable?.should be_false
cat.should be_valid
cat.save.should be_true
end
end
end end

View file

@ -4,6 +4,7 @@ require File.join(FIXTURE_PATH, 'more', 'card')
require File.join(FIXTURE_PATH, 'more', 'invoice') require File.join(FIXTURE_PATH, 'more', 'invoice')
require File.join(FIXTURE_PATH, 'more', 'service') require File.join(FIXTURE_PATH, 'more', 'service')
require File.join(FIXTURE_PATH, 'more', 'event') require File.join(FIXTURE_PATH, 'more', 'event')
require File.join(FIXTURE_PATH, 'more', 'cat')
describe "ExtendedDocument properties" do describe "ExtendedDocument properties" do
@ -94,7 +95,7 @@ describe "ExtendedDocument properties" do
@invoice.location = nil @invoice.location = nil
@invoice.should_not be_valid @invoice.should_not be_valid
@invoice.save.should be_false @invoice.save.should be_false
@invoice.should be_new_document @invoice.should be_new
end end
end end
@ -191,5 +192,69 @@ describe "ExtendedDocument properties" do
end end
end end
end end
describe "a newly created casted model" do
before(:each) do
reset_test_db!
@cat = Cat.new(:name => 'Toonces')
@squeaky_mouse = CatToy.new(:name => 'Squeaky')
end
describe "assigned assigned to a casted property" do
it "should have casted_by set to its parent" do
@squeaky_mouse.casted_by.should be_nil
@cat.favorite_toy = @squeaky_mouse
@squeaky_mouse.casted_by.should === @cat
end
end
describe "appended to a casted collection" do
it "should have casted_by set to its parent" do
@squeaky_mouse.casted_by.should be_nil
@cat.toys << @squeaky_mouse
@squeaky_mouse.casted_by.should === @cat
@cat.save
@cat.toys.first.casted_by.should === @cat
end
end
describe "list assigned to a casted collection" do
it "should have casted_by set on all elements" do
toy1 = CatToy.new(:name => 'Feather')
toy2 = CatToy.new(:name => 'Mouse')
@cat.toys = [toy1, toy2]
toy1.casted_by.should === @cat
toy2.casted_by.should === @cat
@cat.save
@cat = Cat.get(@cat.id)
@cat.toys[0].casted_by.should === @cat
@cat.toys[1].casted_by.should === @cat
end
end
end
describe "a casted model retrieved from the database" do
before(:each) do
reset_test_db!
@cat = Cat.new(:name => 'Stimpy')
@cat.favorite_toy = CatToy.new(:name => 'Stinky')
@cat.toys << CatToy.new(:name => 'Feather')
@cat.toys << CatToy.new(:name => 'Mouse')
@cat.save
@cat = Cat.get(@cat.id)
end
describe "as a casted property" do
it "should already be casted_by its parent" do
@cat.favorite_toy.casted_by.should === @cat
end
end
describe "from a casted collection" do
it "should already be casted_by its parent" do
@cat.toys[0].casted_by.should === @cat
@cat.toys[1].casted_by.should === @cat
end
end
end

View file

@ -26,9 +26,9 @@ class Article < CouchRest::ExtendedDocument
timestamps! timestamps!
save_callback :before, :generate_slug_from_title before_save :generate_slug_from_title
def generate_slug_from_title def generate_slug_from_title
self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new_document? self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?
end end
end end

View file

@ -6,6 +6,7 @@ class Cat < CouchRest::ExtendedDocument
property :name property :name
property :toys, :cast_as => ['CatToy'], :default => [] property :toys, :cast_as => ['CatToy'], :default => []
property :favorite_toy, :cast_as => 'CatToy'
end end
class CatToy < Hash class CatToy < Hash

View file

@ -1,6 +1,7 @@
class Person < Hash class Person < Hash
include ::CouchRest::CastedModel include ::CouchRest::CastedModel
property :name property :name
property :pet, :cast_as => 'Cat'
def last_name def last_name
name.last name.last