search_lingo 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2bdd0cc2315966fc0399d9e24299260dba55e4ed
4
- data.tar.gz: 25f6f3aab4dec1fb3a612cda703bdcd3cb42ed77
3
+ metadata.gz: b4c58f28e8b25ab5ec35bf08089b12ccae7a0f88
4
+ data.tar.gz: 81bfba3fafebdadbe909ff5a6a4506e2fac965cc
5
5
  SHA512:
6
- metadata.gz: 8bb20be38c50a3c4307d60bbe9ad95697fb1b9d526a37ae2c6d832f1356fa3913737f87d8ceb2955a2b5901c24208586d7bbfbf9fe55107ae4e7c5b552471288
7
- data.tar.gz: e5a96234ad506535a278478306a0b7b9d9f0b4c408c75d4d81b55721af385dfa609648cdf8d887ae4abbd0f8c262027a0d256e83fe279e15475cdc5414a5559c
6
+ metadata.gz: e8c453f5e4bd238077618415c6fd93840c3583358279793791ac94936d45152b2c3d07ed895a80f17a9076d064775c6e03042e8940cfcacd9c63ffb5a51322d1
7
+ data.tar.gz: 23077beb4f9a66b6a0f78f84a95939c1abb987d53adddef1650790a7069075fea6b92745a6b3fc44b3aab09699c461fc7eb12098ffa0f9eec99e8f8226f75258
data/.travis.yml CHANGED
@@ -1,3 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 2.1.6
3
4
  - 2.2.2
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # [![Gem Version](https://badge.fury.io/rb/search_lingo.svg)](http://badge.fury.io/rb/search_lingo)
1
+ # [![Gem Version](https://badge.fury.io/rb/search_lingo.svg)](http://badge.fury.io/rb/search_lingo) [![Build Status](https://travis-ci.org/jparker/search_lingo.svg?branch=master)](https://travis-ci.org/jparker/search_lingo)
2
2
 
3
3
  # SearchLingo
4
4
 
@@ -33,7 +33,8 @@ Or install it yourself as:
33
33
 
34
34
  ## Usage
35
35
 
36
- Here is a simple example.
36
+ Concrete examples of how to use this gem are provided in `examples/` and
37
+ `test/examples/`, but here is a simple example.
37
38
 
38
39
  ```ruby
39
40
  class Task < ActiveRecord::Base
@@ -116,10 +117,8 @@ implementation of `#default_parse` in that class. Register parsers for specific
116
117
  types of search tokens using the parser class method.
117
118
 
118
119
  Instantiate your search class by passing in the query string and the scope on
119
- which to perform the search. Use the `#results` method to compile the search
120
- and return the results.
121
-
122
- Take a look at the examples/ directory for more concrete examples.
120
+ which to perform the search. Use the `#results` method to compile and execute
121
+ the search and return the results.
123
122
 
124
123
  ## How It Works
125
124
 
@@ -128,7 +127,7 @@ ActiveRecord model). The search breaks the query string down into a series of
128
127
  tokens, and each token is processed by a declared series of parsers. If a
129
128
  parser succeeds, processing immediately advances to the next token. If none of
130
129
  the declared parsers succeeds, and the token is compound — that is, the token
131
- is composed of an operator and a term (e.g., `foo: bar`), the token is
130
+ is composed of a modifier and a term (e.g., `foo: bar`), the token is
132
131
  simplified and then processed by the declared parsers again. If the second pass
133
132
  also fails, then the (now simplified) token falls through to the
134
133
  `#default_parse` method defined by the search class. This method should be
@@ -137,7 +136,7 @@ or an Array that can be splatted and sent to the search scope.
137
136
 
138
137
  ## Search Classes
139
138
 
140
- Search classes should inherit from `SearchLogic::AbstractSearch`, and they must
139
+ Search classes should inherit from `SearchLingo::AbstractSearch`, and they must
141
140
  provide their own implementation of `#default_parse`. Optionally, a search
142
141
  class may also use the parse class method to add specialized parsers for
143
142
  handling tokens that match specific patterns. As each token is processed, the
@@ -149,15 +148,15 @@ structured.
149
148
  ## Tokenization
150
149
 
151
150
  Queries are comprised of zero or more tokens separated by white space. A token
152
- has a term and an optional operator. (A simple token has no operator; a
151
+ has a term and an optional modifier. (A simple token has no modifier; a
153
152
  compound token does.) A term can be a single word or multiple words joined by
154
153
  spaces and contained within double quotes. For example `foo` and `"foo bar
155
- baz"` are both single terms. An operator is one or more alphanumeric characters
154
+ baz"` are both single terms. A modifier is one or more alphanumeric characters
156
155
  followed by a colon and zero or more spaces.
157
156
 
158
157
  QUERY := TOKEN*
159
- TOKEN := (OPERATOR ':' [[:space:]]*)? TERM
160
- OPERATOR := [[:alnum:]]+
158
+ TOKEN := (MODIFIER ':' [[:space:]]*)? TERM
159
+ MODIFIER := [[:alnum:]]+
161
160
  TERM := '"' [^"]* '"' | [[:graph:]]+
162
161
 
163
162
  The following are all examples of tokens:
@@ -168,22 +167,22 @@ The following are all examples of tokens:
168
167
  * `foo: "bar baz"`
169
168
 
170
169
  (If you need a term to equal something that might otherwise be interpreted as
171
- an operator, you can enclose the term in double quotes, e.g., while `foo: bar`
170
+ a modifier, you can enclose the term in double quotes, e.g., while `foo: bar`
172
171
  would be interpreted a single compound token, `"foo:" bar` would be treated as
173
172
  two distinct simple tokens, and `"foo: bar"` would be treated as a single
174
173
  simple token.)
175
174
 
176
- Tokens are passed to parsers as instances of the SearchLingo::Token class.
177
- SearchLingo::Token provides `#operator` and `#term` methods, but delegates all
178
- other behavior to the String class. Consequently, when writing parsers, you
179
- have the option of either interacting with examining the operator and term
175
+ Tokens are passed to parsers as instances of the `SearchLingo::Token` class.
176
+ `SearchLingo::Token` provides `#modifier` and `#term` methods, but delegates
177
+ all other behavior to the String class. Consequently, when writing parsers, you
178
+ have the option of either interacting with examining the modifier and term
180
179
  individually or treating the entire token as a String and processing it
181
180
  yourself. The following would produce identical results:
182
181
 
183
182
  ```ruby
184
183
  token = SearchLingo::Token.new('foo: "bar baz"')
185
184
 
186
- if token.operator == 'foo' then token.term end # => 'bar baz'
185
+ if token.modifier == 'foo' then token.term end # => 'bar baz'
187
186
  token.match(/\Afoo:\s*"?(.+?)"?\z/) { |m| m[1] } # => 'bar baz'
188
187
  ```
189
188
 
@@ -235,9 +234,9 @@ classes:
235
234
  ```ruby
236
235
  module Parsers
237
236
  class IdParser
238
- def initialize(table, operator = nil)
237
+ def initialize(table, modifier = nil)
239
238
  @table = table
240
- @prefix = /#{operator}:\s*/ if operator
239
+ @prefix = /#{modifier}:\s*/ if modifier
241
240
  end
242
241
 
243
242
  def call(token)
@@ -276,8 +275,12 @@ Additionally, there are parsers for handling closed date ranges (e.g.,
276
275
  `1/1/15-6/30/15`) as well as open-ended date ranges (e.g., `1/1/15-` and
277
276
  `12/31/15`). Look at the files in `lib/search_lingo/parsers` for more details.
278
277
 
279
- As implemented, the date parsers are US-centric. I would like to work on making
280
- them more flexible when time permits.
278
+ The date parser are specifically designed to work with US-formatted dates. Time
279
+ permitting, I will work on making them more flexible. As implemented they are
280
+ also ActiveRecord-centric; this also needs to be reexamined reexamined, either
281
+ by finding a more agnostic implementation or renaming the parser classes to
282
+ indicate they are ActiveRecord-only implementations. (If going the latter
283
+ route, a Sequel-specific implementation should also be provided.)
281
284
 
282
285
  ## Development
283
286
 
data/examples/complex.rb CHANGED
@@ -1,13 +1,13 @@
1
- class Job < ActiveRecord::Base
1
+ class Job < ActiveRecord::Base # :nodoc:
2
2
  # Assume this model has attributes: :id, :date, :name
3
3
  end
4
4
 
5
- class Receipt < ActiveRecord::Base
5
+ class Receipt < ActiveRecord::Base # :nodoc:
6
6
  # Assume this model has attributes: :id, :check_no, :check_date, :post_date, :amount
7
7
  end
8
8
 
9
- module Parsers
10
- class IdParser
9
+ module Parsers # :nodoc:
10
+ class IdParser # :nodoc:
11
11
  def initialize(table)
12
12
  @table = table
13
13
  end
@@ -20,7 +20,7 @@ module Parsers
20
20
  end
21
21
  end
22
22
 
23
- class JobSearch < AbstractSearch
23
+ class JobSearch < SearchLingo::AbstractSearch # :nodoc:
24
24
  parser Parsers::IdParser.new Job.table_name
25
25
 
26
26
  parser SearchLingo::Parsers::DateParser.new Job.table_name,
@@ -35,7 +35,7 @@ class JobSearch < AbstractSearch
35
35
  end
36
36
  end
37
37
 
38
- class ReceiptSearch < AbstractSearch
38
+ class ReceiptSearch < SearchLingo::AbstractSearch # :nodoc:
39
39
  parser Parsers::IdParser.new Receipt.table_name
40
40
 
41
41
  parser SearchLingo::Parsers::DateParser.new Receipt.table_name,
@@ -0,0 +1,64 @@
1
+ require 'sequel'
2
+ require 'sqlite3'
3
+
4
+ DB = Sequel.sqlite
5
+
6
+ if ENV['LOG_TO_STDOUT']
7
+ require 'logger'
8
+ DB.loggers << Logger.new(STDOUT)
9
+ end
10
+
11
+ DB.create_table :categories do
12
+ primary_key :id
13
+ String :name, null: false, unique: true
14
+ end
15
+
16
+ DB.create_table :tasks do
17
+ foreign_key :category_id, :categories
18
+ String :name, null: false, unique: true
19
+ Integer :priority, null: false
20
+ Date :due_date, null: false
21
+ end
22
+
23
+ class Category < Sequel::Model # :nodoc:
24
+ one_to_many :tasks
25
+ end
26
+
27
+ class Task < Sequel::Model # :nodoc:
28
+ many_to_one :category
29
+ end
30
+
31
+ class CategoryParser # :nodoc:
32
+ def call(token)
33
+ if token.modifier == 'cat'
34
+ [:where, { category__name: token.term }]
35
+ end
36
+ end
37
+ end
38
+
39
+ class TaskSearch < SearchLingo::AbstractSearch # :nodoc:
40
+ parser CategoryParser.new
41
+
42
+ parser do |token|
43
+ token.match /\A([<>])([[:digit:]]+)\z/ do |m|
44
+ [:where, ->{ priority.send m[1], m[2] }]
45
+ end
46
+ end
47
+
48
+ parser do |token|
49
+ token.match %r{\A(?<m>\d{1,2})/(?<d>\d{1,2})/(?<y>\d{2}\d{2}?)\z} do |m|
50
+ begin
51
+ [:where, { due_date: Date.parse("#{m[:y]}/#{m[:m]}/#{m[:d]}") }]
52
+ rescue ArgumentError
53
+ end
54
+ end
55
+ end
56
+
57
+ def default_parse(token)
58
+ [:where, 'tasks.name LIKE ?', "%#{token.term}%"]
59
+ end
60
+
61
+ def scope
62
+ @scope.eager_graph(:category)
63
+ end
64
+ end
@@ -2,38 +2,86 @@ require 'search_lingo/tokenizer'
2
2
 
3
3
  module SearchLingo
4
4
  class AbstractSearch
5
+ attr_reader :query
6
+
7
+ ##
8
+ # Instantiates a new search object. +query+ is the string that is to be
9
+ # parsed and compiled into an actual query. If +query+ is falsey, an empty
10
+ # string will be used. +scope+ is the object to which the compiled query
11
+ # should be sent, e.g., an +ActiveRecord+ model.
5
12
  def initialize(query, scope)
6
13
  @query = query || ''
7
14
  @scope = scope
8
15
  end
9
16
 
10
- attr_reader :query
11
-
17
+ ##
18
+ # Returns an list of parsers that have been added to this class.
12
19
  def self.parsers
13
20
  @parsers ||= []
14
21
  end
15
22
 
23
+ ##
24
+ # Adds a new parser to the list of parsers used by this class.
25
+ #
26
+ # The parser to be added can be passed in as an argument which responds to
27
+ # +#call+ or as a block. Raises +ArgumentError+ if neither a callable
28
+ # object nor a block is passed in. If both a callable argument and a block
29
+ # are passed in, a warning will be displayed, the callable argument will be
30
+ # used as the parser; the block will be ignored.
31
+ #
32
+ # class MyParser
33
+ # def call
34
+ # # return something
35
+ # end
36
+ # end
37
+ #
38
+ # class MySearch < SearchLingo::AbstractSearch
39
+ # parser MyParser.new
40
+ # parser do
41
+ # # return something
42
+ # end
43
+ # end
16
44
  def self.parser(callable = nil, &block)
17
45
  unless callable || block_given?
18
46
  raise ArgumentError, 'parse must be called with callable or block'
19
47
  end
20
48
  if callable && block_given?
49
+ # TODO: should this raise an error instead?
21
50
  warn "WARNING: parse called with callable and block (#{caller.first}"
22
51
  end
23
52
 
24
53
  parsers << (callable || block)
25
54
  end
26
55
 
56
+ ##
57
+ # Delegates to SearchLingo::AbstractSearch.parsers.
27
58
  def parsers
28
59
  self.class.parsers
29
60
  end
30
61
 
62
+ ##
63
+ # Returns the results of executing the search.
31
64
  def results
32
65
  @results ||= conditions.inject(scope) do |query, condition|
33
66
  query.public_send(*condition)
34
67
  end
35
68
  end
36
69
 
70
+ ##
71
+ # Returns an +Array+ of compiled query parameters.
72
+ #
73
+ # @query is broken down into tokens, and each token is passed through the
74
+ # list of defined parsers. If a parser is successful, +:match+ is thrown,
75
+ # the compiled condition is saved, and processing moves on to the next
76
+ # token. If none of the parsers succeeds and the token is compound, that
77
+ # is, it has both a modifier and a term, the token is simplified, and
78
+ # reprocessed through the list of parsers. As during the first pass, if a
79
+ # parser succeeds, +:match+ is thrown, the compiled condition for the now
80
+ # simplified token is saved, and processing moves on to the next token (the
81
+ # remains of the original compound token). If none of the parsers succeeds
82
+ # during the second pass, the now simplified token is finally sent to
83
+ # +#default_parse+, and whatever it returns will be saved as the compiled
84
+ # condition.
37
85
  def conditions
38
86
  tokenizer.inject([]) do |conditions, token|
39
87
  conditions << catch(:match) do
@@ -47,10 +95,19 @@ module SearchLingo
47
95
  end
48
96
  end
49
97
 
98
+ ##
99
+ # Returns a +SearchLingo::Tokenizer+ for @query.
50
100
  def tokenizer
51
101
  @tokenizer ||= Tokenizer.new query
52
102
  end
53
103
 
104
+ ##
105
+ # Passes +token+ to each parser in turn. If a parser succeeds, throws
106
+ # +:match+ with the compiled result.
107
+ #
108
+ # A parser succeeds if +call+ returns a truthy value. The return value of a
109
+ # successful parser will be splatted and sent to @scope using
110
+ # +public_send+.
54
111
  def parse(token)
55
112
  parsers.each do |parser|
56
113
  result = parser.call token
@@ -58,11 +115,22 @@ module SearchLingo
58
115
  end
59
116
  end
60
117
 
118
+ ##
119
+ # Raises +NotImplementedError+. Classes which inherit from
120
+ # SearchLingo::AbstractSearch must provide their own implementation, and it
121
+ # should *always* succeed.
61
122
  def default_parse(token)
62
123
  raise NotImplementedError,
63
124
  "#default_parse must be implemented by #{self.class}"
64
125
  end
65
126
 
127
+ ##
128
+ # Returns @scope.
129
+ #
130
+ # You may override this method in your search class if you want to ensure
131
+ # additional messages are sent to search scope before executing the query.
132
+ # For example, if @scope is an +ActiveRecord+ model, you might want to join
133
+ # additional tables.
66
134
  def scope
67
135
  @scope
68
136
  end
@@ -1,4 +1,9 @@
1
1
  module SearchLingo
2
- OPERATOR = /[[:alnum:]]+/
3
- TERM = /"[^"]+"|[[:graph:]]+/
2
+ ##
3
+ # Pattern for matching modifiers within a token.
4
+ MODIFIER = /[[:alnum:]]+/
5
+
6
+ ##
7
+ # Pattern for matching terms within a token.
8
+ TERM = /"[^"]+"|[[:graph:]]+/
4
9
  end
@@ -1,20 +1,24 @@
1
1
  require 'search_lingo/parsers/mdy'
2
2
 
3
3
  module SearchLingo
4
- module Parsers
4
+ module Parsers # :nodoc:
5
5
  class DateParser
6
6
  include MDY
7
7
 
8
- def initialize(table, column, operator = nil, **options)
8
+ def initialize(table, column, modifier = nil, **options)
9
9
  @table = table
10
10
  @column = column
11
- @prefix = %r{#{operator}:\s*} if operator
11
+ @prefix = %r{#{modifier}:\s*} if modifier
12
12
 
13
13
  post_initialize **options
14
14
  end
15
15
 
16
16
  attr_reader :table, :column, :prefix
17
17
 
18
+ # This implementation is specific to ActiveRecord::Base#where semantics.
19
+ # Explore an agnostic implementation or rename the DateParser class (and
20
+ # its descendants) to indicate that it is ActiveRecord-centric. If going
21
+ # the latter route, provide a Sequel-specific implementation as well.
18
22
  def call(token)
19
23
  token.match /\A#{prefix}(?<date>#{US_DATE})\z/ do |m|
20
24
  date = parse m[:date]
@@ -22,10 +26,10 @@ module SearchLingo
22
26
  end
23
27
  end
24
28
 
25
- def post_initialize(**)
29
+ def post_initialize(**) # :nodoc:
26
30
  end
27
31
 
28
- def inspect
32
+ def inspect # :nodoc:
29
33
  '#<%s:0x%x @table=%s @column=%s @prefix=%s>' %
30
34
  [self.class, object_id << 1, table.inspect, column.inspect, prefix.inspect]
31
35
  end
@@ -1,7 +1,7 @@
1
1
  require 'search_lingo/parsers/date_parser'
2
2
 
3
3
  module SearchLingo
4
- module Parsers
4
+ module Parsers # :nodoc:
5
5
  class DateRangeParser < DateParser
6
6
  def call(token)
7
7
  token.match /\A#{prefix}(?<min>#{US_DATE})-(?<max>#{US_DATE})\z/ do |m|
@@ -1,8 +1,8 @@
1
1
  require 'search_lingo/parsers/open_date_range_parser'
2
2
 
3
3
  module SearchLingo
4
- module Parsers
5
- class GTEDateParser < OpenDateRangeParser
4
+ module Parsers # :nodoc:
5
+ class GTEDateParser < OpenDateRangeParser # :nodoc:
6
6
  def initialize(*)
7
7
  warn "DEPRECATION WARNING: use SearchLingo::Parsers::OpenDateRangeParser " \
8
8
  "instead of #{self.class} (from #{caller.first})"
@@ -1,8 +1,8 @@
1
1
  require 'search_lingo/parsers/open_date_range_parser'
2
2
 
3
3
  module SearchLingo
4
- module Parsers
5
- class LTEDateParser < OpenDateRangeParser
4
+ module Parsers # :nodoc:
5
+ class LTEDateParser < OpenDateRangeParser # :nodoc:
6
6
  def initialize(*)
7
7
  warn "DEPRECATION WARNING: use SearchLingo::Parsers::OpenDateRangeParser " \
8
8
  "instead of #{self.class} (from #{caller.first})"
@@ -1,22 +1,37 @@
1
1
  require 'date'
2
2
 
3
3
  module SearchLingo
4
- module Parsers
4
+ module Parsers # :nodoc:
5
5
  module MDY
6
+ ##
7
+ # Pattern for matching US-formatted date strings.
8
+ #
9
+ # The year may be two or four digits, or it may be omitted.
6
10
  US_DATE = %r{(?<m>\d{1,2})/(?<d>\d{1,2})(?:/(?<y>\d{2}\d{2}?))?}
7
11
 
12
+ ##
13
+ # Returns a +Date+ object for the date represented by +term+. Returns
14
+ # +nil+ if +term+ can not be parsed.
15
+ #
16
+ # If the year has two digits, it will be expanded into a four-digit by
17
+ # +Date.parse+.
18
+ #
19
+ # If the year is omitted, it will be inferred using +relative_to+ as a
20
+ # reference date. In this scenario, the resulting date will always be
21
+ # less than or equal to the reference date. If +relative_to+ omitted, it
22
+ # defaults to today's date.
23
+ #
24
+ # Available as both a class method and an instance method.
8
25
  def parse(term, relative_to: Date.today)
9
26
  term.match /\A#{US_DATE}\z/ do |m|
10
27
  return Date.parse "#{m[:y]}/#{m[:m]}/#{m[:d]}" if m[:y]
11
28
 
12
29
  day = Integer(m[:d])
13
30
  month = Integer(m[:m])
14
- year = begin
15
- if month < relative_to.month || month == relative_to.month && day <= relative_to.day
16
- relative_to.year
17
- else
18
- relative_to.year - 1
19
- end
31
+ year = if month < relative_to.month || month == relative_to.month && day <= relative_to.day
32
+ relative_to.year
33
+ else
34
+ relative_to.year - 1
20
35
  end
21
36
 
22
37
  Date.new year, month, day
@@ -2,37 +2,45 @@ require 'search_lingo/parsers/date_parser'
2
2
  require 'forwardable'
3
3
 
4
4
  module SearchLingo
5
- module Parsers
5
+ module Parsers # :nodoc:
6
6
  class OpenDateRangeParser < DateParser
7
7
  extend Forwardable
8
8
 
9
- DATE_RANGE = /(?:-(?<max>#{US_DATE})|(?<min>#{US_DATE})-)/
10
-
11
9
  def call(token)
12
- token.match /\A#{prefix}#{DATE_RANGE}\z/ do |m|
13
- if m[:max]
14
- date = parse m[:max]
10
+ parse_lte(token) || parse_gte(token)
11
+ end
12
+
13
+ def post_initialize(connection:, **)
14
+ @connection = connection
15
+ end
16
+
17
+ def_delegators :@connection, :quote_column_name, :quote_table_name
18
+
19
+ private
20
+
21
+ def parse_lte(token) # :nodoc:
22
+ token.match /\A#{prefix}-(?<date>#{US_DATE})\z/ do |m|
23
+ if date = parse(m[:date])
15
24
  [
16
25
  :where,
17
26
  "#{quote_table_name table}.#{quote_column_name column} <= ?",
18
27
  date
19
- ] if date
20
- else
21
- date = parse m[:min]
28
+ ]
29
+ end
30
+ end
31
+ end
32
+
33
+ def parse_gte(token) # :nodoc:
34
+ token.match /\A#{prefix}(?<date>#{US_DATE})-\z/ do |m|
35
+ if date = parse(m[:date])
22
36
  [
23
37
  :where,
24
38
  "#{quote_table_name table}.#{quote_column_name column} >= ?",
25
39
  date
26
- ] if date
40
+ ]
27
41
  end
28
42
  end
29
43
  end
30
-
31
- def post_initialize(connection:, **)
32
- @connection = connection
33
- end
34
-
35
- def_delegators :@connection, :quote_column_name, :quote_table_name
36
44
  end
37
45
  end
38
46
  end
@@ -3,23 +3,45 @@ require 'search_lingo/constants'
3
3
 
4
4
  module SearchLingo
5
5
  class Token < DelegateClass(String)
6
- STRUCTURE = /\A(?:(#{OPERATOR}):[[:space:]]*)?"?(.+?)"?\z/
6
+ ##
7
+ # Pattern for decomposing a token into a modifier and a term.
8
+ STRUCTURE = /\A(?:(#{MODIFIER}):[[:space:]]*)?"?(.+?)"?\z/
7
9
 
8
- def operator
10
+ ##
11
+ # Returns the modifier portion of the token. Returns +nil+ if token does
12
+ # not have a modifier.
13
+ #
14
+ # Token.new('foo: bar').modifier # => 'foo'
15
+ # Token.new('bar').modifier # => nil
16
+ def modifier
9
17
  self[STRUCTURE, 1]
10
18
  end
11
19
 
20
+ alias_method :operator, :modifier
21
+
22
+ ##
23
+ # Returns the term portion of the token. If the term is wrapped in quotes,
24
+ # they are removed.
25
+ #
26
+ # Token.new('foo: bar').term # => 'bar'
27
+ # Token.new('bar').term # => 'bar'
28
+ # Token.new('"bar baz"').term # => 'bar baz'
12
29
  def term
13
30
  self[STRUCTURE, 2]
14
31
  end
15
32
 
33
+ ##
34
+ # Returns +true+ if token has a modifier and +false+ otherwise.
35
+ #
36
+ # Token.new('foo: bar').compound? # => true
37
+ # Token.new('bar').compound? # => false
16
38
  def compound?
17
- !!operator
39
+ !!modifier
18
40
  end
19
41
 
20
- def inspect
21
- '#<%s String(%s) operator=%s term=%s>' %
22
- [self.class, super, operator.inspect, term.inspect]
42
+ def inspect # :nodoc:
43
+ '#<%s String(%s) modifier=%s term=%s>' %
44
+ [self.class, super, modifier.inspect, term.inspect]
23
45
  end
24
46
  end
25
47
  end
@@ -8,14 +8,25 @@ module SearchLingo
8
8
  include Enumerable
9
9
  extend Forwardable
10
10
 
11
+ ##
12
+ # Pattern for matching a simple token (a term without a modifier).
11
13
  SIMPLE_TOKEN = /#{TERM}/
12
- COMPOUND_TOKEN = /(?:#{OPERATOR}:[[:space:]]*)?#{TERM}/
14
+
15
+ ##
16
+ # Pattern for matching a compound token (a term with an optional modifier).
17
+ COMPOUND_TOKEN = /(?:#{MODIFIER}:[[:space:]]*)?#{TERM}/
18
+
19
+ ##
20
+ # Pattern for matching the delimiter between tokens.
13
21
  DELIMITER = /[[:space:]]*/
14
22
 
15
- def initialize(query)
23
+ def initialize(query) # :nodoc:
16
24
  @scanner = StringScanner.new query.strip
17
25
  end
18
26
 
27
+ ##
28
+ # Iterates over the query string. If called with a block, it yields each
29
+ # token. If called without a block, it returns an +Enumerator+.
19
30
  def each
20
31
  return to_enum(__callee__) unless block_given?
21
32
 
@@ -24,6 +35,9 @@ module SearchLingo
24
35
  end
25
36
  end
26
37
 
38
+ ##
39
+ # Returns a Token for the next token in the query string. When the end of
40
+ # the query string is reached raises +StopIteration+.
27
41
  def next
28
42
  scanner.skip DELIMITER
29
43
  token = scanner.scan COMPOUND_TOKEN
@@ -33,6 +47,9 @@ module SearchLingo
33
47
 
34
48
  def_delegator :scanner, :reset
35
49
 
50
+ ##
51
+ # Rewinds the query string from the last returned token and returns a
52
+ # Token for the next simple token.
36
53
  def simplify
37
54
  scanner.unscan
38
55
  Token.new scanner.scan SIMPLE_TOKEN
@@ -1,3 +1,3 @@
1
1
  module SearchLingo
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
data/search_lingo.gemspec CHANGED
@@ -19,11 +19,14 @@ Gem::Specification.new do |spec|
19
19
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
20
  spec.require_paths = ["lib"]
21
21
 
22
+ spec.rdoc_options += ['-x', 'examples/', '-x', 'test/']
23
+
22
24
  spec.required_ruby_version = '>= 2.1'
23
25
 
24
26
  spec.add_development_dependency "bundler", "~> 1.9"
25
27
  spec.add_development_dependency "rake", "~> 10.0"
26
28
  spec.add_development_dependency 'minitest'
27
- spec.add_development_dependency 'minitest-focus'
28
29
  spec.add_development_dependency 'pry'
30
+ spec.add_development_dependency 'sequel'
31
+ spec.add_development_dependency 'sqlite3'
29
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search_lingo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Parker
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-06-11 00:00:00.000000000 Z
11
+ date: 2015-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,7 +53,7 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: minitest-focus
56
+ name: pry
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
@@ -67,7 +67,21 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: pry
70
+ name: sequel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - ">="
@@ -97,7 +111,7 @@ files:
97
111
  - bin/console
98
112
  - bin/setup
99
113
  - examples/complex.rb
100
- - examples/simple.rb
114
+ - examples/sequel_example.rb
101
115
  - lib/search_lingo.rb
102
116
  - lib/search_lingo/abstract_search.rb
103
117
  - lib/search_lingo/constants.rb
@@ -117,7 +131,11 @@ licenses:
117
131
  - MIT
118
132
  metadata: {}
119
133
  post_install_message:
120
- rdoc_options: []
134
+ rdoc_options:
135
+ - "-x"
136
+ - examples/
137
+ - "-x"
138
+ - test/
121
139
  require_paths:
122
140
  - lib
123
141
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -132,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
150
  version: '0'
133
151
  requirements: []
134
152
  rubyforge_project:
135
- rubygems_version: 2.4.7
153
+ rubygems_version: 2.4.8
136
154
  signing_key:
137
155
  specification_version: 4
138
156
  summary: Framework for building simple query languages
data/examples/simple.rb DELETED
@@ -1,31 +0,0 @@
1
- class Task < ActiveRecord::Base
2
- # Assume this model has attributes: :id, :due_date, and :name
3
- end
4
-
5
- class TaskSearch < SearchLingo::AbstractSearch
6
- parser SearchLingo::Parsers::DateParser.new :tasks,
7
- :due_date
8
- parser SearchLingo::Parsers::DateRangeParser.new :tasks,
9
- :due_date
10
- parser SearchLingo::Parsers::LTEDateParser.new :tasks,
11
- :due_date, connection: ActiveRecord::Base.connection
12
- parser SearchLingo::Parsers::GTEDateParser.new :tasks,
13
- :due_date, connection: ActiveRecord::Base.connection
14
-
15
- parser do |token|
16
- token.match /\Aid:\s*([[:digit:]]+)\z/ do |m|
17
- [:where, { tasks: { id: m[1] } }]
18
- end
19
- end
20
-
21
- def default_parse(token)
22
- [:where, 'tasks.name LIKE ?', "%#{token}%"]
23
- end
24
- end
25
-
26
- search = TaskSearch.new('6/4/15 id: 42 foo "bar baz"', Task)
27
- search.results # => Task
28
- # .where(tasks: { due_date: Date.new(2015, 6, 4) })
29
- # .where(tasks: { id: '42' })
30
- # .where('tasks.name LIKE ?', '%foo%')
31
- # .where('tasks.name LIKE ?', '%bar baz%')