lib/rosette/core/path_matcher_factory.rb
# 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