module Subtrigger # A Rule object knows when to fire some kind of action for some # kind of revision. When the Subversion hook is fired, a Rule can inspect it # and choose whether or not to fire its trigger (a piece code defined by the # user). # # In the first example, the rule will output fired whenever a # Revision comes along with a message containing foo. # # In the second example, we find all applicable rules for a given # Revision object. We can then run each of them. # # @example 1: Define a simple Rule # Rule.new(/foo/) { puts 'fired' } # # @example 2: Finding and firing Rules # rev = Revision.new # Rule.matching(rev).map { |rule| rule.run(rev) } # # @since 0.3.0 # @author Arjan van der Gaag class Rule # Exception for when trying to apply a rule to something other than an # instance of Revision. CannotCompare = Class.new(Exception) # A hash of Revision attributes and regular expressions to match against attr_reader :criteria # The callback to run on a match attr_reader :block private @rules = [] # Keep track of Rule objects that are created in a class instance variable # # @param [Rule] child is the new Rule object # @return [Array] the total list of children def self.register(child) @rules << child end public # Return an array of all rules currently defined. # # @return [Array] def self.rules @rules end # Reset the list of known rules, deleting all currently known rules. # # @return nil def self.reset @rules = [] end # Return an array of all existing Rule objects that match the given # revision. # # @param [Revision] revision is the revision to compare rules to. # @return [Array] list of all matching rules def self.matching(revision) @rules.select { |child| child === revision } end # Create a new Rule object with criteria for different properties of a # Revision. The required block defines the callback to run. It will have # the current Revision object yielded to it. # # Criteria are Ruby objects that should match (`===`) a Revision's # attributes. These would usually be regular expressions, but they # can be strings or custom objects if you want to. # # @overload initialize(pattern, &block) # Define a rule with a pattern matching the log message # @param [Regex] pattern is the regular expression to match against # the revision's log message # @overload initialize(options, &block) # Define a rule with various criteria in a hash. # @param [Hash] options defines matching criteria. # @option options :author Criterium for Revision#author # @option options :date Criterium for Revision#date # @option options :number Criterium for Revision#number # @option options :project Criterium for Revision#project def initialize(pattern_or_options, &block) raise ArgumentError, 'a Rule requires a block' unless block_given? # If not given a hash, we build a hash defaulting on message unless pattern_or_options.is_a?(Hash) pattern_or_options = { :message => pattern_or_options } end @criteria, @block = pattern_or_options, block @criteria.inspect self.class.register self end # Call this Rule's callback method with the give Revision object. # @return [nil] def run(rev) @rev = rev block.call(@rev, collect_captures) end # Use {Rule#matches?} to see if this Rule matches the given # Revision. # # @param [Object] the object to compare to # @return [Boolean] # @see Rule#matches? def ===(other) matches?(other) rescue CannotCompare super end # See if the current rule matches a given subversion revision. # # @param [Revision] revision the Revision object to compare to. # @return [Boolean] # @see Rule#=== # @raise Subtrigger::Rule::CannotCompare when comparing to something other # than a revision. def matches?(revision) raise CannotCompare unless @criteria.keys.all? { |k| k == :all || revision.respond_to?(k) } match = @criteria.any? @criteria.each_pair do |key, value| if key == :all match = (value === revision) else match &= (value === revision.send(key.to_sym)) end end match end private # When using regular expressions to match against string values, we # want to be able to get to any captured groups. This method scans all # string values with their Regex matchers and collects all captured # groups into a namespaced hash. # # @example # Rule.new /hello, (.+)!/ do |revision, matches| # puts matches.inspect # end # # => { :message => ['world'] } # # @return [Hash] all captured groups per Revision attribute tested # @todo this only passes on capture groups, not the entire match ($&) def collect_captures criteria.inject({}) do |output, (key, value)| next if key == :all output[key] = @rev.send(key.to_sym).scan(value).flatten if value.is_a?(Regexp) output end end end end