module Lotus module Utils # Before and After callbacks # # @since 0.1.0 # @private module Callbacks # Series of callbacks to be executed # # @since 0.1.0 # @private class Chain # Return a new chain # # @return [Lotus::Utils::Callbacks::Chain] # # @since 0.2.0 def initialize @chain = Array.new end # Adds the given callbacks to the chain # # @param callbacks [Array] one or multiple callbacks to add # @param blk [Proc] an optional block to be added # # @return [void] # # @raise [RuntimeError] if the object was previously frozen # # @see #run # @see Lotus::Utils::Callbacks::Callback # @see Lotus::Utils::Callbacks::MethodCallback # @see Lotus::Utils::Callbacks::Chain#freeze # # @since 0.1.0 # # @example # require 'lotus/utils/callbacks' # # chain = Lotus::Utils::Callbacks::Chain.new # # # Add a Proc to be used as a callback, it will be wrapped by `Callback` # # The optional argument(s) correspond to the one passed when invoked the chain with `run`. # chain.add { Authenticator.authenticate! } # chain.add {|params| ArticleRepository.find(params[:id]) } # # # Add a Symbol as a reference to a method name that will be used as a callback. # # It will wrapped by `MethodCallback` # # If the #notificate method accepts some argument(s) they should be passed when `run` is invoked. # chain.add :notificate def add(*callbacks, &blk) callbacks.push blk if block_given? callbacks.each do |c| @chain.push Factory.fabricate(c) end @chain.uniq! end # Runs all the callbacks in the chain. # The only two ways to stop the execution are: `raise` or `throw`. # # @param context [Object] the context where we want the chain to be invoked. # @param args [Array] the arguments that we want to pass to each single callback. # # @since 0.1.0 # # @example # require 'lotus/utils/callbacks' # # class Action # private # def authenticate! # end # # def set_article(params) # end # end # # action = Action.new # params = Hash[id: 23] # # chain = Lotus::Utils::Callbacks::Chain.new # chain.add :authenticate!, :set_article # # chain.run(action, params) # # # `params` will only be passed as #set_article argument, because it has an arity greater than zero # # # # chain = Lotus::Utils::Callbacks::Chain.new # # chain.add do # # some authentication logic # end # # chain.add do |params| # # some other logic that requires `params` # end # # chain.run(action, params) # # Those callbacks will be invoked within the context of `action`. def run(context, *args) @chain.each do |callback| callback.call(context, *args) end end # It freezes the object by preventing further modifications. # # @since 0.2.0 # # @see http://ruby-doc.org/core-2.1.2/Object.html#method-i-freeze # # @example # require 'lotus/utils/callbacks' # # chain = Lotus::Utils::Callbacks::Chain.new # chain.freeze # # chain.frozen? # => true # # chain.add :authenticate! # => RuntimeError def freeze super @chain.freeze end end # Callback factory # # @since 0.1.0 # @private class Factory # Instantiates a `Callback` according to if it responds to #call. # # @param callback [Object] the object that needs to be wrapped # # @return [Callback, MethodCallback] # # @since 0.1.0 # # @example # require 'lotus/utils/callbacks' # # callable = Proc.new{} # it responds to #call # method = :upcase # it doesn't responds to #call # # Lotus::Utils::Callbacks::Factory.fabricate(callable).class # # => Lotus::Utils::Callbacks::Callback # # Lotus::Utils::Callbacks::Factory.fabricate(method).class # # => Lotus::Utils::Callbacks::MethodCallback def self.fabricate(callback) if callback.respond_to?(:call) Callback.new(callback) else MethodCallback.new(callback) end end end # Proc callback # It wraps an object that responds to #call # # @since 0.1.0 # @private class Callback attr_reader :callback # Initialize by wrapping the given callback # # @param callback [Object] the original callback that needs to be wrapped # # @return [Callback] self # # @since 0.1.0 # def initialize(callback) @callback = callback end # Executes the callback within the given context and passing the given arguments. # # @param context [Object] the context within we want to execute the callback. # @param args [Array] an array of arguments that will be available within the execution. # # @return [void, Object] It may return a value, it depends on the callback. # # @since 0.1.0 # # @see Lotus::Utils::Callbacks::Chain#run def call(context, *args) context.instance_exec(*args, &callback) end end # Method callback # It wraps a symbol or a string representing a method name that is implemented by the context within it will be called. # # @since 0.1.0 # @private class MethodCallback < Callback # Executes the callback within the given context and eventually passing the given arguments. # Those arguments will be passed according to the arity of the target method. # # @param context [Object] the context within we want to execute the callback. # @param args [Array] an array of arguments that will be available within the execution. # # @return [void, Object] It may return a value, it depends on the callback. # # @since 0.1.0 # # @see Lotus::Utils::Callbacks::Chain#run def call(context, *args) method = context.method(callback) if method.parameters.any? method.call(*args) else method.call end end protected def hash callback.hash end def eql?(other) hash == other.hash end end end end end