Skip to content

Commit

Permalink
Extract MessageMatcher from Command
Browse files Browse the repository at this point in the history
  • Loading branch information
kou committed Feb 7, 2014
1 parent 58b935c commit 21139d1
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 94 deletions.
84 changes: 5 additions & 79 deletions lib/droonga/command.rb
Expand Up @@ -13,98 +13,24 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

require "droonga/message_matcher"

module Droonga
class Command
attr_reader :method_name
#
#
# @option options [Array] :pattern The pattern to be matched
# against message. If the pattern is matched to a message,
# the command will be applied.
#
# Here is pattern syntax.
#
# * PATTERN = [TARGET_PATH, OPERATOR, ARGUMENTS*]
# * PATTERN = [PATTERN, LOGICAL_OPERATOR, PATTERN]
# * TARGET_PATH = "COMPONENT(.COMPONENT)*"
# * OPERATOR = :equal, :in, :include?, :exist?
# (More operators may be added in the future.
# For example, :start_with and so on.)
# * ARGUMENTS = OBJECT_DEFINED_IN_JSON*
# * LOGICAL_OPERATOR = :or (:add will be added.)
#
# For example:
#
# ```
# ["type", :equal, "search"]
# ```
#
# matches to the following message:
#
# ```
# {"type" => "search"}
# ```
#
# Another example:
#
# ```
# ["body.output.limit", :equal, 10]
# ```
#
# matches to the following message:
#
# ```
# {
# "body" => {
# "output" => {
# "limit" => 10,
# },
# },
# }
# ```
# @see MessageMatcher
def initialize(method_name, options)
@method_name = method_name
@options = options
@matcher = MessageMatcher.new(@options[:pattern])
end

def match?(message)
match_pattern?(@options[:pattern], message)
end

private
def match_pattern?(pattern, message)
return false if pattern.nil?
path, operator, *arguments = pattern
target = resolve_path(path, message)
apply_operator(operator, target, arguments)
end

NONEXISTENT_PATH = Object.new
def resolve_path(path, message)
path.split(".").inject(message) do |result, component|
return NONEXISTENT_PATH unless result.is_a?(Hash)
result[component]
end
end

def apply_operator(operator, target, arguments)
case operator
when :equal
[target] == arguments
when :in
arguments.any? do |argument|
argument.include?(target)
end
when :include?
return false unless target.respond_to?(:include?)
arguments.any? do |argument|
target.include?(argument)
end
when :exist?
target != NONEXISTENT_PATH
else
raise ArgumentError, "Unknown operator"
end
@matcher.match?(message)
end
end
end
101 changes: 101 additions & 0 deletions lib/droonga/message_matcher.rb
@@ -0,0 +1,101 @@
# Copyright (C) 2014 Droonga Project
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 2.1 as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

module Droonga
# It checks whether the pattern matches against a message.
#
# It provides the small language. Here is the pattern syntax.
#
# * PATTERN = [TARGET_PATH, OPERATOR, ARGUMENTS*]
# * PATTERN = [PATTERN, LOGICAL_OPERATOR, PATTERN]
# * TARGET_PATH = "COMPONENT(.COMPONENT)*"
# * OPERATOR = :equal, :in, :include?, :exist?
# (More operators may be added in the future.
# For example, :start_with and so on.)
# * ARGUMENTS = OBJECT_DEFINED_IN_JSON*
# * LOGICAL_OPERATOR = :or (:add will be added.)
#
# For example:
#
# ```
# ["type", :equal, "search"]
# ```
#
# matches to the following message:
#
# ```
# {"type" => "search"}
# ```
#
# Another example:
#
# ```
# ["body.output.limit", :equal, 10]
# ```
#
# matches to the following message:
#
# ```
# {
# "body" => {
# "output" => {
# "limit" => 10,
# },
# },
# }
# ```
class MessageMatcher
# @param [Array] pattern The pattern to be matched against a message.
def initialize(pattern)
@pattern = pattern
end

def match?(message)
return false if @pattern.nil?
path, operator, *arguments = @pattern
target = resolve_path(path, message)
apply_operator(operator, target, arguments)
end

private
NONEXISTENT_PATH = Object.new
def resolve_path(path, message)
path.split(".").inject(message) do |result, component|
return NONEXISTENT_PATH unless result.is_a?(Hash)
result[component]
end
end

def apply_operator(operator, target, arguments)
case operator
when :equal
[target] == arguments
when :in
arguments.any? do |argument|
argument.include?(target)
end
when :include?
return false unless target.respond_to?(:include?)
arguments.any? do |argument|
target.include?(argument)
end
when :exist?
target != NONEXISTENT_PATH
else
raise ArgumentError, "Unknown operator"
end
end
end
end
22 changes: 7 additions & 15 deletions test/unit/test_command.rb → test/unit/test_message_matcher.rb
Expand Up @@ -13,24 +13,20 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

require "droonga/command"
require "droonga/message_matcher"

class CommandTest < Test::Unit::TestCase
def command(method_name, options={})
Droonga::Command.new(method_name, options)
class MessageMatcherTest < Test::Unit::TestCase
def matcher(pattern)
Droonga::MessageMatcher.new(pattern)
end

class ResolvePathTest < self
def command
super(:method_name)
end

def resolve_path(path, message)
command.send(:resolve_path, path, message)
matcher(nil).send(:resolve_path, path, message)
end

def test_nonexistent
assert_equal(Droonga::Command::NONEXISTENT_PATH,
assert_equal(Droonga::MessageMatcher::NONEXISTENT_PATH,
resolve_path("nonexistent.path", {}))
end

Expand All @@ -56,12 +52,8 @@ def test_nested
end

class MatchTest < self
def command(pattern)
super(:method_name, :pattern => pattern)
end

def match?(pattern, message)
command(pattern).match?(message)
matcher(pattern).match?(message)
end

class EqualTest < self
Expand Down

0 comments on commit 21139d1

Please sign in to comment.