Integrated Yehuda's new callback code from rails

This commit is contained in:
Peter Gumeson 2009-06-07 02:57:22 -07:00
parent 1c6e073b47
commit dc4787e905
8 changed files with 228 additions and 221 deletions

View file

@ -69,20 +69,20 @@ 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 }
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,8 @@
require File.join(File.dirname(__FILE__), '..', 'support', 'class') require File.join(File.dirname(__FILE__), '..', 'support', 'class')
# Extracted from ActiveSupport::Callbacks written by Yehuda Katz # Extracted from ActiveSupport::NewCallbacks written by Yehuda Katz
# http://github.com/wycats/rails/raw/abstract_controller/activesupport/lib/active_support/new_callbacks.rb # http://github.com/rails/rails/raw/d6e4113c83a9d55be6f2af247da2cecaa855f43b/activesupport/lib/active_support/new_callbacks.rb
# http://github.com/wycats/rails/raw/18b405f154868204a8f332888871041a7bad95e1/activesupport/lib/active_support/callbacks.rb # http://github.com/rails/rails/commit/1126a85aed576402d978e6f76eb393b6baaa9541
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
@ -94,9 +94,8 @@ module CouchRest
@@_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)
@ -134,9 +133,8 @@ module CouchRest
@@_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
@ -289,7 +287,22 @@ module CouchRest
filter 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)" : "") method_name << case filter.arity
when 1
"(self)"
when 2
" self, Proc.new "
else
""
end
when Method
@klass.send(:define_method, "#{method_name}_method") { filter }
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{method_name}(&blk)
#{method_name}_method.call(self, &blk)
end
RUBY_EVAL
method_name
when String when String
@klass.class_eval <<-RUBY_EVAL @klass.class_eval <<-RUBY_EVAL
def #{method_name} def #{method_name}
@ -298,22 +311,35 @@ module CouchRest
RUBY_EVAL RUBY_EVAL
method_name method_name
else else
kind, name = @kind, @name kind = @kind
@klass.send(:define_method, method_name) do @klass.send(:define_method, "#{method_name}_object") { filter }
filter.send("#{kind}_#{name}", self)
_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
method_name method_name
end end
end end
def _normalize_legacy_filter(kind, filter)
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
filter.metaclass.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
# 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 end
# An Array with a compile method # An Array with a compile method
@ -328,7 +354,7 @@ module CouchRest
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
@ -342,7 +368,7 @@ module CouchRest
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
@ -355,41 +381,42 @@ 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, callbacks)
str = <<-RUBY_EVAL body = callbacks.compile(nil, :terminator => send("_#{symbol}_terminator"))
def _run_#{symbol}_callbacks(key = nil)
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}_callbacks").
compile(name, :object => obj, :terminator => send("_#{kind}_terminator"))
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 class_eval "def #{name}() #{str} end", __FILE__, __LINE__
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.
@ -423,58 +450,62 @@ 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
responded = self.respond_to?(":_#{name}_callbacks")
callbacks = send("_#{name}_callbacks")
yield callbacks, type, filters, options if block_given?
_define_runner(name, callbacks)
end
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}_callbacks=", 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)
extlib_inheritable_accessor("_#{symbol}_callbacks") do
CallbackChain.new(symbol)
end
self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 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)
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.map! do |filter|
# overrides parent class
self._#{symbol}_callbacks.delete_if {|c| c.matches?(type, :#{symbol}, filter)}
Callback.new(filter, type, options.dup, self, :#{symbol})
end
self._#{symbol}_callbacks.push(*filters)
_define_runner(:#{symbol},
self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
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
_define_runner(:#{symbol},
self._#{symbol}_callbacks.compile(nil, :terminator => _#{symbol}_terminator),
options)
end
end
def self.reset_#{symbol}_callbacks def self.reset_#{symbol}_callbacks
self._#{symbol}_callbacks = CallbackChain.new(:#{symbol}) update_callbacks(:#{symbol})
_define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {})
end end
self.#{symbol}_callback(:before) self.set_callback(:#{symbol}, :before)
RUBY_EVAL RUBY_EVAL
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

@ -62,7 +62,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,96 +1,55 @@
# Copyright (c) 2004-2008 David Heinemeier Hansson # Extracted From
# # http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2
# 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.
# Allows attributes to be shared within an inheritance hierarchy, but where # Extends the class object with class and instance accessors for class attributes,
# each descendant gets a copy of their parents' attributes, instead of just a # just like the native attr* accessors for instance attributes.
# 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 # class Person
# parent, siblings, or children, which is unlike the regular class-level # cattr_accessor :hair_colors
# attributes that are shared across the entire hierarchy. # 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 +115,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 +129,24 @@ 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.respond_to?(:extract_options!)
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| set_callback :validate, :before do |object|
object.run_before_validate = true object.run_before_validate = true
end end
validate_callback :after do |object| set_callback :validate, :after do |object|
object.run_after_validate = true object.run_after_validate = true
end end
end end

View file

@ -29,28 +29,28 @@ describe "ExtendedDocument" do
property :run_before_update property :run_before_update
property :run_after_update property :run_after_update
validate_callback :before do |object| set_callback :validate, :before do |object|
object.run_before_validate = true object.run_before_validate = true
end end
validate_callback :after do |object| set_callback :validate, :after do |object|
object.run_after_validate = true object.run_after_validate = true
end end
save_callback :before do |object| set_callback :save, :before do |object|
object.run_before_save = true object.run_before_save = true
end end
save_callback :after do |object| set_callback :save, :after do |object|
object.run_after_save = true object.run_after_save = true
end end
create_callback :before do |object| set_callback :create, :before do |object|
object.run_before_create = true object.run_before_create = true
end end
create_callback :after do |object| set_callback :create, :after do |object|
object.run_after_create = true object.run_after_create = true
end end
update_callback :before do |object| set_callback :update, :before do |object|
object.run_before_update = true object.run_before_update = true
end end
update_callback :after do |object| set_callback :update, :after do |object|
object.run_after_update = true object.run_after_update = true
end end
end end

View file

@ -26,7 +26,7 @@ class Article < CouchRest::ExtendedDocument
timestamps! timestamps!
save_callback :before, :generate_slug_from_title set_callback :save, :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? self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new?