Merge branch 'new_callbacks'

This commit is contained in:
Peter Gumeson 2009-07-18 23:37:16 -07:00
commit 7bae8acc36
8 changed files with 344 additions and 228 deletions

View file

@ -69,20 +69,26 @@ CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument
### Callbacks ### Callbacks
`CouchRest::ExtendedDocuments` instances have 4 callbacks already defined for you: `CouchRest::ExtendedDocuments` instances have 4 callbacks already defined for you:
`validate_callback`, `create_callback`, `save_callback`, `update_callback` and `destroy_callback` `:validate`, `:create`, `:save`, `:update` and `:destroy`
`CouchRest::CastedModel` instances have 1 callback already defined for you: `CouchRest::CastedModel` instances have 1 callback already defined for you:
`validate_callback` `:validate`
Define your callback as follows: 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 new shorter version:
before_save :before_method, :another_method
after_save :after_method, :another_method, :if => :condition
around_save {|r| stuff; yield; stuff }
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.

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__
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__
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

@ -77,7 +77,7 @@ module CouchRest
base.class_eval <<-EOS, __FILE__, __LINE__ base.class_eval <<-EOS, __FILE__, __LINE__
define_callbacks :validate 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

View file

@ -32,10 +32,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
@ -59,7 +59,7 @@ module CouchRest
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? object['created_at'] = object['updated_at'] if object.new?
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

@ -35,10 +35,10 @@ class WithCastedCallBackModel < Hash
property :run_before_validate property :run_before_validate
property :run_after_validate property :run_after_validate
validate_callback :before do |object| before_validate do |object|
object.run_before_validate = true object.run_before_validate = true
end end
validate_callback :after do |object| after_validate do |object|
object.run_after_validate = true object.run_after_validate = true
end end
end end

View file

@ -30,30 +30,56 @@ describe "ExtendedDocument" do
property :run_before_update property :run_before_update
property :run_after_update property :run_after_update
validate_callback :before do |object| before_validate do |object|
object.run_before_validate = true object.run_before_validate = true
end end
validate_callback :after do |object| after_validate do |object|
object.run_after_validate = true object.run_after_validate = true
end end
save_callback :before do |object| 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
@ -552,6 +578,27 @@ describe "ExtendedDocument" do
@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

View file

@ -26,7 +26,7 @@ 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? self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?