From dc4787e90575fe63368ed34c152f2e4cc7fa10e4 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sun, 7 Jun 2009 02:57:22 -0700 Subject: [PATCH 1/5] Integrated Yehuda's new callback code from rails --- README.md | 12 +- lib/couchrest/mixins/callbacks.rb | 273 +++++++++++++---------- lib/couchrest/mixins/validation.rb | 2 +- lib/couchrest/more/extended_document.rb | 2 +- lib/couchrest/support/class.rb | 138 +++++------- spec/couchrest/more/casted_model_spec.rb | 4 +- spec/couchrest/more/extended_doc_spec.rb | 16 +- spec/fixtures/more/article.rb | 2 +- 8 files changed, 228 insertions(+), 221 deletions(-) diff --git a/README.md b/README.md index b9ed345..6312b4f 100644 --- a/README.md +++ b/README.md @@ -69,20 +69,20 @@ CouchRest::Model has been deprecated and replaced by CouchRest::ExtendedDocument ### Callbacks `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: - `validate_callback` + `: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: - save_callback :before, :before_method - save_callback :after, :after_method, :if => :condition - save_callback :around {|r| stuff; yield; stuff } + set_callback :save, :before, :before_method + set_callback :save, :after, :after_method, :if => :condition + set_callback :save, :around {|r| stuff; yield; stuff } Check the mixin or the ExtendedDocument class to see how to implement your own callbacks. diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb index 2516702..ca9a7c5 100644 --- a/lib/couchrest/mixins/callbacks.rb +++ b/lib/couchrest/mixins/callbacks.rb @@ -1,8 +1,8 @@ require File.join(File.dirname(__FILE__), '..', 'support', 'class') -# Extracted from ActiveSupport::Callbacks written by Yehuda Katz -# 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 +# 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 module CouchRest # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic @@ -85,19 +85,18 @@ module CouchRest def self.included(klass) klass.extend ClassMethods end - + def run_callbacks(kind, options = {}, &blk) send("_run_#{kind}_callbacks", &blk) end - + class Callback @@_callback_sequence = 0 - + 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 - @name = name - + normalize_options!(options) @per_key = options.delete(:per_key) @@ -108,7 +107,7 @@ module CouchRest _compile_per_key_options end - + def clone(klass) obj = super() obj.klass = klass @@ -120,7 +119,7 @@ module CouchRest obj.options[:unless] = @options[:unless].dup obj end - + def normalize_options!(options) options[:if] = Array(options[:if]) options[:unless] = Array(options[:unless]) @@ -129,14 +128,13 @@ module CouchRest options[:per_key][:if] = Array(options[:per_key][:if]) options[:per_key][:unless] = Array(options[:per_key][:unless]) end - + def next_id @@_callback_sequence += 1 end - - def matches?(_kind, _name, _filter) + + def matches?(_kind, _filter) @kind == _kind && - @name == _name && @filter == _filter end @@ -144,11 +142,11 @@ module CouchRest filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless) filter_options[:unless].push(new_options[:if]) if new_options.key?(:if) end - + def recompile!(_options, _per_key) _update_filter(self.options, _options) _update_filter(self.per_key, _per_key) - + @callback_id = next_id @filter = _compile_filter(@raw_filter) @compiled_options = _compile_options(@options) @@ -164,19 +162,19 @@ module CouchRest end RUBY_EVAL end - + # This will supply contents for before and around filters, and no # contents for after filters (for the forward pass). def start(key = nil, options = {}) object, terminator = (options || {}).values_at(:object, :terminator) - + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") - + terminator ||= false - + # options[0] is the compiled form of supplied conditions # options[1] is the "end" for the conditional - + if @kind == :before || @kind == :around if @kind == :before # if condition # before_save :filter_name, :if => :condition @@ -204,7 +202,7 @@ module CouchRest # yield self # end # end - + name = "_conditional_callback_#{@kind}_#{next_id}" txt = <<-RUBY_EVAL def #{name}(halted) @@ -222,14 +220,14 @@ module CouchRest end end end - + # This will supply contents for around and after filters, but not # before filters (for the backward pass). def end(key = nil, options = {}) object = (options || {})[:object] - + return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?") - + if @kind == :around || @kind == :after # if condition # after_save :filter_name, :if => :condition # filter_name @@ -241,27 +239,27 @@ module CouchRest end end end - + private # Options support the same options as filters themselves (and support # symbols, string, procs, and objects), so compile a conditional # expression based on the options def _compile_options(options) return [] if options[:if].empty? && options[:unless].empty? - + conditions = [] - + unless options[:if].empty? conditions << Array(_compile_filter(options[:if])) end - + unless options[:unless].empty? conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} end - + ["if #{conditions.flatten.join(" && ")}", "end"] end - + # Filters support: # Arrays:: Used in conditions. This is used to specify # multiple conditions. Used internally to @@ -289,7 +287,22 @@ module CouchRest filter when Proc @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 @klass.class_eval <<-RUBY_EVAL def #{method_name} @@ -298,52 +311,65 @@ module CouchRest RUBY_EVAL method_name else - kind, name = @kind, @name - @klass.send(:define_method, method_name) do - filter.send("#{kind}_#{name}", self) - end + kind = @kind + @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 + RUBY_EVAL + method_name 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 - # An Array with a compile method class CallbackChain < Array def initialize(symbol) @symbol = symbol end - + def compile(key = nil, options = {}) method = [] method << "halted = false" each do |callback| method << callback.start(key, options) end - method << "yield self if block_given?" + method << "yield self if block_given? && !halted" reverse_each do |callback| method << callback.end(key, options) end method.compact.join("\n") end - + def clone(klass) chain = CallbackChain.new(@symbol) chain.push(*map {|c| c.clone(klass)}) end end - + 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 # 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. @@ -355,43 +381,44 @@ module CouchRest # The _run_save_callbacks method can optionally take a key, which # will be used to compile an optimized callback method for each # key. See #define_callbacks for more information. - def _define_runner(symbol, str, options) - str = <<-RUBY_EVAL - def _run_#{symbol}_callbacks(key = nil) + def _define_runner(symbol, callbacks) + body = callbacks.compile(nil, :terminator => send("_#{symbol}_terminator")) + + body, line = <<-RUBY_EVAL, __LINE__ + def _run_#{symbol}_callbacks(key = nil, &blk) 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 - #{str} + #{body} end end RUBY_EVAL - - class_eval str, __FILE__, __LINE__ + 1 - - before_name, around_name, after_name = - options.values_at(:before, :after, :around) + + undef_method "_run_#{symbol}_callbacks" if method_defined?("_run_#{symbol}_callbacks") + class_eval body, __FILE__, line end - + # This is called the first time a callback is called with a particular # key. It creates a new callback method for the key, calculating # 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[[kind, key]] ||= begin - str = self.send("_#{kind}_callbacks").compile(key, :object => obj, :terminator => self.send("_#{kind}_terminator")) + @_keyed_callbacks[name] ||= begin + str = send("_#{kind}_callbacks"). + 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 end - - obj.send("_run__#{klass.split("::").last}__#{kind}__#{key}__callbacks", &blk) end - + # Define callbacks. # # Creates a _callback method that you can use to add callbacks. @@ -423,58 +450,62 @@ module CouchRest # In that case, each action_name would get its own compiled callback # method that took into consideration the per_key conditions. This # 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) terminator = symbols.pop if symbols.last.is_a?(String) symbols.each do |symbol| - self.extlib_inheritable_accessor("_#{symbol}_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}) + extlib_inheritable_accessor("_#{symbol}_terminator") { terminator } - 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 - + extlib_inheritable_accessor("_#{symbol}_callbacks") do + CallbackChain.new(symbol) + end + + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def self.reset_#{symbol}_callbacks - self._#{symbol}_callbacks = CallbackChain.new(:#{symbol}) - _define_runner(:#{symbol}, self._#{symbol}_callbacks.compile, {}) + update_callbacks(:#{symbol}) end - - self.#{symbol}_callback(:before) + + self.set_callback(:#{symbol}, :before) RUBY_EVAL end end diff --git a/lib/couchrest/mixins/validation.rb b/lib/couchrest/mixins/validation.rb index 467e143..015770a 100644 --- a/lib/couchrest/mixins/validation.rb +++ b/lib/couchrest/mixins/validation.rb @@ -77,7 +77,7 @@ module CouchRest base.class_eval <<-EOS, __FILE__, __LINE__ define_callbacks :validate if method_defined?(:_run_save_callbacks) - save_callback :before, :check_validations + set_callback :save, :before, :check_validations end EOS base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index 44c73c3..b8c84ac 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -62,7 +62,7 @@ module CouchRest property(:updated_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['created_at'] = object['updated_at'] if object.new? end diff --git a/lib/couchrest/support/class.rb b/lib/couchrest/support/class.rb index ecbab76..a33c2d5 100644 --- a/lib/couchrest/support/class.rb +++ b/lib/couchrest/support/class.rb @@ -1,96 +1,55 @@ -# Copyright (c) 2004-2008 David Heinemeier Hansson +# Extracted From +# http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2 + +# Extends the class object with class and instance accessors for class attributes, +# just like the native attr* accessors for instance attributes. # -# 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: +# class Person +# cattr_accessor :hair_colors +# end # -# 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 -# each descendant gets a copy of their parents' attributes, instead of just a -# 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 -# parent, siblings, or children, which is unlike the regular class-level -# attributes that are shared across the entire hierarchy. +# Person.hair_colors = [:brown, :black, :blonde, :red] class Class - # Defines class-level and instance-level attribute reader. - # - # @param *syms Array of attributes to define reader for. - # @return 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) syms.flatten.each do |sym| next if sym.is_a?(Hash) - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - - def #{sym} - @@#{sym} - end - RUBY + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + unless defined? @@#{sym} # unless defined? @@hair_colors + @@#{sym} = nil # @@hair_colors = nil + end # end + # + def self.#{sym} # def self.hair_colors + @@#{sym} # @@hair_colors + end # end + # + def #{sym} # def hair_colors + @@#{sym} # @@hair_colors + end # end + EOS end end unless Class.respond_to?(:cattr_reader) - # Defines class-level (and optionally instance-level) attribute writer. - # - # @param Boolean}]> Array of attributes to define writer for. - # @option syms :instance_writer if true, instance-level attribute writer is defined. - # @return List of attributes that were made into cattr_writers - # - # @api public def cattr_writer(*syms) - options = syms.last.is_a?(Hash) ? syms.pop : {} + options = syms.extract_options! syms.flatten.each do |sym| - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - RUBY - - unless options[:instance_writer] == false - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - def #{sym}=(obj) - @@#{sym} = obj - end - RUBY - end + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + unless defined? @@#{sym} # unless defined? @@hair_colors + @@#{sym} = nil # @@hair_colors = nil + end # end + # + def self.#{sym}=(obj) # def self.hair_colors=(obj) + @@#{sym} = obj # @@hair_colors = obj + end # end + # + #{" # + def #{sym}=(obj) # def hair_colors=(obj) + @@#{sym} = obj # @@hair_colors = obj + end # end + " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false + EOS end end unless Class.respond_to?(:cattr_writer) - # Defines class-level (and optionally instance-level) attribute accessor. - # - # @param *syms Boolean}]> Array of attributes to define accessor for. - # @option syms :instance_writer if true, instance-level attribute writer is defined. - # @return List of attributes that were made into accessors - # - # @api public def cattr_accessor(*syms) cattr_reader(*syms) cattr_writer(*syms) @@ -156,6 +115,8 @@ class Class def #{ivar}=(obj) self.class.#{ivar} = obj end RUBY end + + self.send("#{ivar}=", yield) if block_given? end end unless Class.respond_to?(:extlib_inheritable_writer) @@ -168,9 +129,24 @@ class Class # @return An Array of attributes turned into inheritable accessors. # # @api public - def extlib_inheritable_accessor(*syms) + def extlib_inheritable_accessor(*syms, &block) extlib_inheritable_reader(*syms) - extlib_inheritable_writer(*syms) + extlib_inheritable_writer(*syms, &block) end unless Class.respond_to?(:extlib_inheritable_accessor) 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 + diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index 029e294..e81fe16 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -35,10 +35,10 @@ class WithCastedCallBackModel < Hash property :run_before_validate property :run_after_validate - validate_callback :before do |object| + set_callback :validate, :before do |object| object.run_before_validate = true end - validate_callback :after do |object| + set_callback :validate, :after do |object| object.run_after_validate = true end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index d4100bf..1a752ac 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -29,28 +29,28 @@ describe "ExtendedDocument" do property :run_before_update property :run_after_update - validate_callback :before do |object| + set_callback :validate, :before do |object| object.run_before_validate = true end - validate_callback :after do |object| + set_callback :validate, :after do |object| object.run_after_validate = true end - save_callback :before do |object| + set_callback :save, :before do |object| object.run_before_save = true end - save_callback :after do |object| + set_callback :save, :after do |object| object.run_after_save = true end - create_callback :before do |object| + set_callback :create, :before do |object| object.run_before_create = true end - create_callback :after do |object| + set_callback :create, :after do |object| object.run_after_create = true end - update_callback :before do |object| + set_callback :update, :before do |object| object.run_before_update = true end - update_callback :after do |object| + set_callback :update, :after do |object| object.run_after_update = true end end diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index 840b45b..aa26e1d 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -26,7 +26,7 @@ class Article < CouchRest::ExtendedDocument timestamps! - save_callback :before, :generate_slug_from_title + set_callback :save, :before, :generate_slug_from_title def generate_slug_from_title self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new? From 806a79a72aa517bb0280ea5a5ad995e820858263 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sun, 7 Jun 2009 14:52:23 -0700 Subject: [PATCH 2/5] Enabled callback halting and added copyright notices --- lib/couchrest/mixins/callbacks.rb | 27 ++++++++++++++++++++++--- lib/couchrest/more/extended_document.rb | 8 ++++---- lib/couchrest/support/class.rb | 21 +++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb index ca9a7c5..7daf708 100644 --- a/lib/couchrest/mixins/callbacks.rb +++ b/lib/couchrest/mixins/callbacks.rb @@ -1,9 +1,30 @@ -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 +require File.join(File.dirname(__FILE__), '..', 'support', 'class') + module CouchRest # Callbacks are hooks into the lifecycle of an object that allow you to trigger logic # before or after an alteration of the object state. @@ -368,7 +389,7 @@ module CouchRest end module ClassMethods - CHAINS = {:before => :before, :around => :before, :after => :after} + #CHAINS = {:before => :before, :around => :before, :after => :after} # 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 diff --git a/lib/couchrest/more/extended_document.rb b/lib/couchrest/more/extended_document.rb index b8c84ac..2e2bdfc 100644 --- a/lib/couchrest/more/extended_document.rb +++ b/lib/couchrest/more/extended_document.rb @@ -32,10 +32,10 @@ module CouchRest attr_accessor :casted_by # Callbacks - define_callbacks :create - define_callbacks :save - define_callbacks :update - define_callbacks :destroy + define_callbacks :create, "result == :halt" + define_callbacks :save, "result == :halt" + define_callbacks :update, "result == :halt" + define_callbacks :destroy, "result == :halt" def initialize(passed_keys={}) apply_defaults # defined in CouchRest::Mixins::Properties diff --git a/lib/couchrest/support/class.rb b/lib/couchrest/support/class.rb index a33c2d5..d8d6337 100644 --- a/lib/couchrest/support/class.rb +++ b/lib/couchrest/support/class.rb @@ -1,3 +1,24 @@ +# 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 # http://github.com/rails/rails/commit/971e2438d98326c994ec6d3ef8e37b7e868ed6e2 From ab362cb32ca067e7134c0ac21a4c823b3866949d Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sun, 7 Jun 2009 17:01:21 -0700 Subject: [PATCH 3/5] More convenient callback alias methods --- README.md | 6 ++++++ lib/couchrest/mixins/callbacks.rb | 10 ++++++++++ spec/couchrest/more/casted_model_spec.rb | 4 ++-- spec/couchrest/more/extended_doc_spec.rb | 16 ++++++++-------- spec/fixtures/more/article.rb | 2 +- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6312b4f..62baa0c 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,12 @@ CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted f set_callback :save, :after, :after_method, :if => :condition set_callback :save, :around {|r| stuff; yield; stuff } + Or the new shorter version: + + before_save :before_method + after_save :after_method, :if => :condition + around_save {|r| stuff; yield; stuff } + Check the mixin or the ExtendedDocument class to see how to implement your own callbacks. ### Casting diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb index 7daf708..5703762 100644 --- a/lib/couchrest/mixins/callbacks.rb +++ b/lib/couchrest/mixins/callbacks.rb @@ -528,6 +528,16 @@ module CouchRest self.set_callback(:#{symbol}, :before) RUBY_EVAL + + # Define more convenient callback methods + # set_callback(:save, :before) becomes before_save + [:before, :after, :around].each do |filter| + self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def self.#{filter}_#{symbol}(meth=nil, &blk) + set_callback(:#{symbol}, :#{filter}, meth||blk) + end + RUBY_EVAL + end end end end diff --git a/spec/couchrest/more/casted_model_spec.rb b/spec/couchrest/more/casted_model_spec.rb index e81fe16..4383b2e 100644 --- a/spec/couchrest/more/casted_model_spec.rb +++ b/spec/couchrest/more/casted_model_spec.rb @@ -35,10 +35,10 @@ class WithCastedCallBackModel < Hash property :run_before_validate property :run_after_validate - set_callback :validate, :before do |object| + before_validate do |object| object.run_before_validate = true end - set_callback :validate, :after do |object| + after_validate do |object| object.run_after_validate = true end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 1a752ac..09513fa 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -29,28 +29,28 @@ describe "ExtendedDocument" do property :run_before_update property :run_after_update - set_callback :validate, :before do |object| + before_validate do |object| object.run_before_validate = true end - set_callback :validate, :after do |object| + after_validate do |object| object.run_after_validate = true end - set_callback :save, :before do |object| + before_save do |object| object.run_before_save = true end - set_callback :save, :after do |object| + after_save do |object| object.run_after_save = true end - set_callback :create, :before do |object| + before_create do |object| object.run_before_create = true end - set_callback :create, :after do |object| + after_create do |object| object.run_after_create = true end - set_callback :update, :before do |object| + before_update do |object| object.run_before_update = true end - set_callback :update, :after do |object| + after_update do |object| object.run_after_update = true end end diff --git a/spec/fixtures/more/article.rb b/spec/fixtures/more/article.rb index aa26e1d..6f4bb7a 100644 --- a/spec/fixtures/more/article.rb +++ b/spec/fixtures/more/article.rb @@ -26,7 +26,7 @@ class Article < CouchRest::ExtendedDocument timestamps! - set_callback :save, :before, :generate_slug_from_title + before_save :generate_slug_from_title def generate_slug_from_title self['slug'] = title.downcase.gsub(/[^a-z0-9]/,'-').squeeze('-').gsub(/^\-|\-$/,'') if new? From db7829e9966ff61cd1ce529bb0bd0ed91837af27 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sun, 7 Jun 2009 18:46:30 -0700 Subject: [PATCH 4/5] Callbacks now take multiple callback methods --- README.md | 4 +- lib/couchrest/mixins/callbacks.rb | 14 ++++++- spec/couchrest/more/extended_doc_spec.rb | 47 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 62baa0c..a169b70 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,8 @@ CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted f Or the new shorter version: - before_save :before_method - after_save :after_method, :if => :condition + 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. diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb index 5703762..9aa02a8 100644 --- a/lib/couchrest/mixins/callbacks.rb +++ b/lib/couchrest/mixins/callbacks.rb @@ -533,13 +533,23 @@ module CouchRest # set_callback(:save, :before) becomes before_save [:before, :after, :around].each do |filter| self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.#{filter}_#{symbol}(meth=nil, &blk) - set_callback(:#{symbol}, :#{filter}, meth||blk) + def self.#{filter}_#{symbol}(*symbols, &blk) + _alias_callbacks(symbols, blk) do |callback, options| + set_callback(:#{symbol}, :#{filter}, callback, options) + end end RUBY_EVAL end end end + + def _alias_callbacks(callbacks, block) + options = callbacks.last.is_a?(Hash) ? callbacks.pop : {} + callbacks.push(block) if block + callbacks.each do |callback| + yield callback, options + end + end end end end diff --git a/spec/couchrest/more/extended_doc_spec.rb b/spec/couchrest/more/extended_doc_spec.rb index 09513fa..7ab5746 100644 --- a/spec/couchrest/more/extended_doc_spec.rb +++ b/spec/couchrest/more/extended_doc_spec.rb @@ -53,6 +53,32 @@ describe "ExtendedDocument" do after_update do |object| object.run_after_update = true 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 class WithTemplateAndUniqueID < CouchRest::ExtendedDocument @@ -532,6 +558,27 @@ describe "ExtendedDocument" do @doc.save.should be_true @doc.run_after_save.should be_true 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 describe "create" do it "should run the before save filter when creating" do From f4f3946d86f412a5d8398d8d93f59eb1e5a08396 Mon Sep 17 00:00:00 2001 From: Peter Gumeson Date: Sat, 13 Jun 2009 22:41:16 -0700 Subject: [PATCH 5/5] Merged in more new callback stuff from rails 3 --- lib/couchrest/mixins/callbacks.rb | 83 +++++++++++-------------------- lib/couchrest/support/class.rb | 19 ++++++- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/lib/couchrest/mixins/callbacks.rb b/lib/couchrest/mixins/callbacks.rb index 9aa02a8..f03055b 100644 --- a/lib/couchrest/mixins/callbacks.rb +++ b/lib/couchrest/mixins/callbacks.rb @@ -142,12 +142,12 @@ module CouchRest end def normalize_options!(options) - options[:if] = Array(options[:if]) - options[:unless] = Array(options[:unless]) + options[:if] = Array.wrap(options[:if]) + options[:unless] = Array.wrap(options[:unless]) options[:per_key] ||= {} - options[:per_key][:if] = Array(options[:per_key][:if]) - options[:per_key][:unless] = Array(options[:per_key][:unless]) + options[:per_key][:if] = Array.wrap(options[:per_key][:if]) + options[:per_key][:unless] = Array.wrap(options[:per_key][:unless]) end def next_id @@ -204,9 +204,10 @@ module CouchRest filter = <<-RUBY_EVAL unless halted result = #{@filter} - halted ||= (#{terminator}) + halted = (#{terminator}) end RUBY_EVAL + [@compiled_options[0], filter, @compiled_options[1]].compact.join("\n") else # Compile around filters with conditions into proxy methods @@ -225,7 +226,7 @@ module CouchRest # end name = "_conditional_callback_#{@kind}_#{next_id}" - txt = <<-RUBY_EVAL + txt, line = <<-RUBY_EVAL, __LINE__ def #{name}(halted) #{@compiled_options[0] || "if true"} && !halted #{@filter} do @@ -236,7 +237,7 @@ module CouchRest end end RUBY_EVAL - @klass.class_eval(txt) + @klass.class_eval(txt, __FILE__, line) "#{name}(halted) do" end end @@ -271,11 +272,11 @@ module CouchRest conditions = [] unless options[:if].empty? - conditions << Array(_compile_filter(options[:if])) + conditions << Array.wrap(_compile_filter(options[:if])) end unless options[:unless].empty? - conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} + conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"} end ["if #{conditions.flatten.join(" && ")}", "end"] @@ -306,33 +307,14 @@ module CouchRest filter.map {|f| _compile_filter(f)} when Symbol filter + when String + "(#{filter})" when Proc @klass.send(:define_method, method_name, &filter) - 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 - @klass.class_eval <<-RUBY_EVAL - def #{method_name} - #{filter} - end - RUBY_EVAL - method_name + return method_name if filter.arity == 0 + + method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") else - kind = @kind @klass.send(:define_method, "#{method_name}_object") { filter } _normalize_legacy_filter(kind, filter) @@ -349,7 +331,7 @@ module CouchRest def _normalize_legacy_filter(kind, filter) if !filter.respond_to?(kind) && filter.respond_to?(:filter) - filter.metaclass.class_eval( + 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 @@ -402,8 +384,9 @@ module CouchRest # The _run_save_callbacks method can optionally take a key, which # will be used to compile an optimized callback method for each # key. See #define_callbacks for more information. - def _define_runner(symbol, callbacks) - body = callbacks.compile(nil, :terminator => send("_#{symbol}_terminator")) + def _define_runner(symbol) + body = send("_#{symbol}_callback"). + compile(nil, :terminator => send("_#{symbol}_terminator")) body, line = <<-RUBY_EVAL, __LINE__ def _run_#{symbol}_callbacks(key = nil, &blk) @@ -431,7 +414,7 @@ module CouchRest def _create_keyed_callback(name, kind, obj, &blk) @_keyed_callbacks ||= {} @_keyed_callbacks[name] ||= begin - str = send("_#{kind}_callbacks"). + str = send("_#{kind}_callback"). compile(name, :object => obj, :terminator => send("_#{kind}_terminator")) class_eval "def #{name}() #{str} end", __FILE__, __LINE__ @@ -471,21 +454,21 @@ module CouchRest # In that case, each action_name would get its own compiled callback # method that took into consideration the per_key conditions. This # is a speed improvement for ActionPack. - def update_callbacks(name, filters = CallbackChain.new(name), block = nil) + 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") + callbacks = send("_#{name}_callback") yield callbacks, type, filters, options if block_given? - _define_runner(name, callbacks) + _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| + _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) } @@ -497,9 +480,9 @@ module CouchRest end def skip_callback(name, *filters, &block) - update_callbacks(name, filters, block) do |callbacks, type, filters, options| + _update_callbacks(name, filters, block) do |callbacks, type, filters, options| filters.each do |filter| - callbacks = send("_#{name}_callbacks=", callbacks.clone(self)) + callbacks = send("_#{name}_callback=", callbacks.clone(self)) filter = callbacks.find {|c| c.matches?(type, filter) } @@ -517,17 +500,11 @@ module CouchRest symbols.each do |symbol| extlib_inheritable_accessor("_#{symbol}_terminator") { terminator } - extlib_inheritable_accessor("_#{symbol}_callbacks") do + extlib_inheritable_accessor("_#{symbol}_callback") do CallbackChain.new(symbol) end - self.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.reset_#{symbol}_callbacks - update_callbacks(:#{symbol}) - end - - self.set_callback(:#{symbol}, :before) - RUBY_EVAL + _define_runner(symbol) # Define more convenient callback methods # set_callback(:save, :before) becomes before_save diff --git a/lib/couchrest/support/class.rb b/lib/couchrest/support/class.rb index d8d6337..5e538ab 100644 --- a/lib/couchrest/support/class.rb +++ b/lib/couchrest/support/class.rb @@ -168,6 +168,23 @@ class Array # options(1, 2, :a => :b) # => {:a=>:b} def extract_options! last.is_a?(::Hash) ? pop : {} - end unless Array.respond_to?(:extract_options!) + 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