class SearchLingo::AbstractSearch

AbstractSearch is an abstract implementation from which search classes should inherit.

Search classes are instantiated with a query string and a default scope on which to perform the search.

Child classes must implement the default_parse instance method, and they may optionally register one or more parsers.

class MySearch < SearchLingo::AbstractSearch
  def default_parse(token, chain)
    chain.where attribute: token.term
  end
end

class MyOtherSearch < SearchLingo::AbstractSearch
  parser SearchLingo::Parsers::DateParser.new Job.arel_table[:date]

  parser do |token, chain|
    token.match(/\Aid: [[:space:]]* (?<id>[[:digit:]]+)\z/x) do |m|
      chain.where id: m[:id]
    end
  end

  def default_parse(token, chain)
    chain.where Job.arel_table[:name].matches "%#{token.term}%"
  end
end

Attributes

logger[R]
query[R]
scope[R]

Public Class Methods

new(query, scope, logger: nil) click to toggle source

Instantiates a new search object. query is the string that is to be parsed and compiled into an actual query. If query is falsey, an empty string will be used. scope is the object to which the compiled query should be sent, e.g., an ActiveRecord::Relation.

MySearchClass.new 'foo bar: baz "froz quux"', Task.all
# File lib/search_lingo/abstract_search.rb, line 45
def initialize(query, scope, logger: nil)
  @query  = query || ''
  @scope  = scope
  @logger = logger
end
parser(parser = nil, &block) click to toggle source

Adds a new parser to the list of parsers used by this class.

The parser may be given as an anonymous block or as any argument which responds to #call. The parser will be send #call with a single argument which will be a token from the query string.

If both a callable object and a block are given, or if neither a callable object nor a block are given, an ArgumentError will be raised.

class MyParser
  def call(token)
    # return something
  end
end

class MySearch < SearchLingo::AbstractSearch
  parser MyParser.new
  parser do |token|
    # return something
  end
end
# File lib/search_lingo/abstract_search.rb, line 79
def self.parser(parser = nil, &block)
  unless block_given? ^ parser.respond_to?(:call)
    raise ArgumentError, 'parse must be called with callable OR block'
  end

  parsers << (parser || block)
end
parsers() click to toggle source

Returns an list of parsers that have been added to this class.

# File lib/search_lingo/abstract_search.rb, line 53
def self.parsers
  @parsers ||= []
end

Public Instance Methods

default_parse(_token, _chain) click to toggle source

The default way to handle a token which could not be parsed by any of the other parsers.

This is a skeletal implementation that raises NotImplementedError. Child classes should provide their own implementation. At a minimum, that implementation should return chain. (Doing so would ignore token.)

# File lib/search_lingo/abstract_search.rb, line 162
def default_parse(_token, _chain)
  raise NotImplementedError,
    "#default_parse must be implemented by #{self.class}"
end
load_results() click to toggle source

Load search results by composing query string tokens into a query chain.

@query is broken down into tokens and parses each one in turn. The results of parsing each token are chained onto the end of scope to compose the query.

# File lib/search_lingo/abstract_search.rb, line 105
def load_results
  tokenizer.reduce(scope) do |chain, token|
    parse token, chain
  end
end
parse(token, chain) click to toggle source

Passes token and chain through the array of parsers until :match is thrown. If none of the parsers match and the token is compound, simplifies the token and reruns the parsers. If no parsers match after the second pass or if the token was not compound, falls back on `#default_parse`.

# File lib/search_lingo/abstract_search.rb, line 123
def parse(token, chain)
  catch(:match) do
    run_parsers token, chain

    if token.compound?
      token = tokenizer.simplify
      run_parsers token, chain
    end

    logger&.debug "default_parse token=#{token.inspect}"
    default_parse token, chain
  end
end
parsers() click to toggle source

Delegates to SearchLingo::AbstractSearch.parsers.

# File lib/search_lingo/abstract_search.rb, line 89
def parsers
  self.class.parsers
end
results() click to toggle source

Returns the results of executing the search.

# File lib/search_lingo/abstract_search.rb, line 95
def results
  @results ||= load_results
end
run_parsers(token, chain) click to toggle source

Passes token to each parser in turn. If a parser succeeds, throws :match with the result.

A parser succeeds if call returns a truthy value. A successful parser will typically send something to chain and return the result. In this way, the tokens of the search are reduced into a composed query.

# File lib/search_lingo/abstract_search.rb, line 144
def run_parsers(token, chain)
  parsers.each do |parser|
    result = parser.call token, chain
    if result
      logger&.debug "parser:#{parser.inspect} token=#{token.inspect}"
      throw :match, result
    end
  end
  nil
end
tokenizer() click to toggle source

Returns a SearchLingo::Tokenizer for @query.

# File lib/search_lingo/abstract_search.rb, line 113
def tokenizer
  @tokenizer ||= Tokenizer.new query
end