rosette-proj/rosette-core

View on GitHub
lib/rosette/core/path_matcher_factory.rb

Summary

Maintainability
A
0 mins
Test Coverage
# encoding: UTF-8

module Rosette
  module Core
    # Constructs condition trees for path matchers.
    #
    # @see ExtractorConfig
    class PathMatcherFactory
      # Creates a new empty node that can be used as the root of a
      # conditions tree.
      #
      # @return [Node] the new empty node
      def self.create_root
        Node.new
      end

      # Facilitates creating operator nodes that can perform binary
      # operations, like "and", "or", and "not".
      module NodeOperatorFactory
        # Creates an {OrNode} for combining two nodes together with a
        # logical "or".
        #
        # @param [Node] right The other node. The left node is +self+.
        # @return [OrNode] a node representing the logical "or" of +self+
        #   and +right+.
        def or(right)
          OrNode.new(self, right)
        end

        # Creates an {AndNode} for combining two nodes together with a
        # logical "and".
        #
        # @param [Node] right The other node. The left node is +self+.
        # @return [AndNode] a node representing the logical "and" of +self+
        #   and +right+.
        def and(right)
          AndNode.new(self, right)
        end

        # Creates a {NotNode} for negating +self+.
        #
        # @return [NotNode] a node representing the negation of +self+.
        def not
          NotNode.new(self)
        end
      end

      # Provides common methods for creating nodes.
      module NodeFactory
        # Creates a {FileExtensionNode}.
        #
        # @param [String] extension The file extension to match.
        # @return [FileExtensionNode]
        def match_file_extension(extension)
          FileExtensionNode.new(extension)
        end

        # Creates a bunch of {FileExtensionNode}s combined using a logical "or".
        #
        # @param [Array<String>] extensions A list of file extensions.
        # @return [FileExtensionNode, OrNode] the root node of a tree of all
        #   the file extensions specified in +extensions+. Each file extension
        #   will be wrapped in a {FileExtensionNode} and logically "or"ed
        #   together. If +extensions+ only contains one file extension, then this
        #   method just returns an instance of {FileExtensionNode}. If +extensions+
        #   contains more than one entry, this method returns an {OrNode}.
        def match_file_extensions(extensions)
          Array(extensions).inject(nil) do |node, extension|
            new_node = match_file_extension(extension)
            node ? node.or(new_node) : new_node
          end
        end

        # Creates a {PathNode}.
        #
        # @param [String] path The path to match.
        # @return [PathNode]
        def match_path(path)
          PathNode.new(path)
        end

        # Creates a bunch of {PathNode}s combined using a logical "or".
        #
        # @param [Array<String>] paths A list of paths.
        # @return [PathNode, OrNode] the root of a tree of all the paths specified
        #   in +paths+. Each path will be wrapped in a {PathNode} and logically
        #   "or"ed together. If +paths+ only contains one path, then this method
        #   just returns an instance of {PathNode}. If +paths+ contains more than
        #   one entry, this method returns an {OrNode}.
        def match_paths(paths)
          Array(paths).inject(nil) do |node, path|
            new_node = match_path(path)
            node ? node.or(new_node) : new_node
          end
        end

        # Creates a {RegexNode}.
        #
        # @param [Regexp] regex The regex to match.
        # @return [RegexNode]
        def match_regex(regex)
          RegexNode.new(regex)
        end

        # Creates a bunch of {RegexNode}s combined using a logical "or".
        #
        # @param [Array<Regexp>] regexes A list of regular expressions.
        # @return [RegexNode, OrNode] the root of a tree of all the regexes specified
        #   in +regexes+. Each regex will be wrapped in a {RegexNode} and logically
        #   "or"ed together. If +regexes+ only contains one entry, then this method
        #   just returns an instance of {RegexNode}. If +regexes+ contains more than
        #   one entry, this method returns an {OrNode}.
        def match_regexes(regexes)
          Array(regexes).inject(nil) do |node, regex|
            new_node = match_regex(regex)
            node ? node.or(new_node) : new_node
          end
        end
      end

      include NodeFactory

      # The base class for all condition nodes.
      class Node
        include NodeFactory
        include NodeOperatorFactory

        # Determines if the given path matches the conditions defined by this node
        # and it's children.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if +path+ matches, false otherwise.
        def matches?(path)
          false
        end
      end

      # The base class for all nodes that perform binary operations (i.e.
      # operations that take two operands).
      #
      # @!attribute [r] left
      #   @return [Node] the left child.
      # @!attribute [r] right
      #   @return [Node] the right child.
      class BinaryNode < Node
        attr_reader :left, :right

        # Creates a new binary node with left and right children.
        #
        # @param [Node] left The left child.
        # @param [Node] right The right child.
        def initialize(left, right)
          @left = left
          @right = right
        end
      end

      # The base class for all nodes that perform unary operations (i.e.
      # operations that take only one operand).
      #
      # @!attribute [r] child
      #   @return [Node] the child node.
      class UnaryNode < Node
        attr_reader :child

        # Creates a new unary node.
        #
        # @param [Node] child The child.
        def initialize(child)
          @child = child
        end
      end

      # A logical "and".
      class AndNode < BinaryNode
        # Determines if the given path matches the left AND the right child's
        # conditions.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if both the left and right children match
        #   +path+, false otherwise.
        def matches?(path)
          left.matches?(path) && right.matches?(path)
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "(#{left.to_s} AND #{right.to_s})"
        end
      end

      # A logical "OR".
      class OrNode < BinaryNode
        # Determines if the given path matches the left OR the right child's
        # conditions.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if the left or the right child matches +path+,
        #   false otherwise.
        def matches?(path)
          left.matches?(path) || right.matches?(path)
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "(#{left.to_s} OR #{right.to_s})"
        end
      end

      # A logical "NOT".
      class NotNode < UnaryNode
        # Determines if the given path does NOT match the child's conditions.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if the child does not match +path+, false
        #   otherwise.
        def matches?(path)
          !child.matches?(path)
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "(NOT #{child.to_s})"
        end
      end

      # Matches file extensions.
      #
      # @!attribute [r] extension
      #   @return [String] the extension to match.
      class FileExtensionNode < Node
        attr_reader :extension

        # Creates a new file extension node.
        #
        # @param [String] extension The extension to match.
        def initialize(extension)
          @extension = extension
        end

        # Determines if the given path's file extension matches +extension+.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if the path matches +extension+, false otherwise.
        def matches?(path)
          # avoid using File.extname to allow matching against double extensions,
          # eg. file.html.erb
          path[-extension.size..-1] == extension
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "has_file_extension('#{extension}')"
        end
      end

      # Matches paths.
      #
      # @!attribute [r] path
      #   @return [String] the path to match.
      class PathNode < Node
        attr_reader :path

        # Creates a new path node.
        #
        # @param [String] path The path to match.
        def initialize(path)
          @path = path
        end

        # Determines if the given path matches +path+.
        #
        # @param [String] match_path The path to match.
        # @return [Boolean] true if +match_path+ matches +path+, false otherwise.
        #   Matching is done by comparing the first +n+ characters of +match_path+
        #   to +path+, where +n+ is the number of characters in +path+. In other words,
        #   if +path+ is "/path/to" and +match_path+ is '/path/to/foo.rb', this method
        #   will return true.
        def matches?(match_path)
          match_path[0...path.size] == path
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "matches_path('#{path}')"
        end
      end

      # Determines if the given path matches +regex+.
      #
      # @!attribute [r] regex
      #   @return [Regex] The regex to match with.
      class RegexNode < Node
        attr_reader :regex

        # Creates a new regex node.
        #
        # @param [Regex] regex The regex to match with.
        def initialize(regex)
          @regex = regex
        end

        # Determines if +regex+ matches the given path.
        #
        # @param [String] path The path to match.
        # @return [Boolean] true if +regex+ matches +path+, false otherwise.
        def matches?(path)
          !!(path =~ regex)
        end

        # Generates a string representation of this node.
        #
        # @return [String]
        def to_s
          "matches_regex(/#{regex.source}/)"
        end
      end
    end
  end
end