search_lingo 1.0.2 → 1.0.3

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
- SHA1:
3
- metadata.gz: b4c58f28e8b25ab5ec35bf08089b12ccae7a0f88
4
- data.tar.gz: 81bfba3fafebdadbe909ff5a6a4506e2fac965cc
2
+ SHA256:
3
+ metadata.gz: 473413fa017d4fe273ea20952b89c778fd07d98735efaeef1c28954bc70c96ea
4
+ data.tar.gz: 11df6b8d3f02b8147afc5a7248a548861b038c8e7e9dffd854dca506e185fa7e
5
5
  SHA512:
6
- metadata.gz: e8c453f5e4bd238077618415c6fd93840c3583358279793791ac94936d45152b2c3d07ed895a80f17a9076d064775c6e03042e8940cfcacd9c63ffb5a51322d1
7
- data.tar.gz: 23077beb4f9a66b6a0f78f84a95939c1abb987d53adddef1650790a7069075fea6b92745a6b3fc44b3aab09699c461fc7eb12098ffa0f9eec99e8f8226f75258
6
+ metadata.gz: a6783a0cb4a8d8db96ddaf4a9cff6704484a6421f93351de009202a7f680c46a50d44c62672e7201a7cdaeb3999ac9df7646b1905471cfef43ef2c9c2e6ef87d
7
+ data.tar.gz: bed0ac621a52ffc67fc50ee32f5dd560f58a3f11444741bc3bae63a9be8f1d0e7e59e0656b5d1cab07cc2a05c1c200889d408c1c7142ceab31416a156edc9d38
data/.travis.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.6
4
- - 2.2.2
3
+ - 2.4.4
4
+ - 2.5.1
data/README.md CHANGED
@@ -276,11 +276,10 @@ Additionally, there are parsers for handling closed date ranges (e.g.,
276
276
  `12/31/15`). Look at the files in `lib/search_lingo/parsers` for more details.
277
277
 
278
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.)
279
+ permitting, I will work on making them more flexible.
280
+
281
+ As implemented they generate queries using AREL. In the future, we should try
282
+ generalizing this behavior to also support Sequel for generating queries.
284
283
 
285
284
  ## Development
286
285
 
data/examples/complex.rb CHANGED
@@ -21,36 +21,18 @@ module Parsers # :nodoc:
21
21
  end
22
22
 
23
23
  class JobSearch < SearchLingo::AbstractSearch # :nodoc:
24
+ parser SearchLingo::Parsers::DateParser.new Job.arel_table[:date]
24
25
  parser Parsers::IdParser.new Job.table_name
25
26
 
26
- parser SearchLingo::Parsers::DateParser.new Job.table_name,
27
- :date
28
- parser SearchLingo::Parsers::DateRangeParser.new Job.table_name,
29
- :date
30
- parser SearchLingo::Parsers::OpenDateRangeParser.new Job.table_name,
31
- :date, connection: Job.connection
32
-
33
27
  def default_parse(token)
34
- [:where, 'jobs.name LIKE ?', "%#{token}%"]
28
+ [:where, Job.arel_table[:name].lower.like("%#{token}%")]
35
29
  end
36
30
  end
37
31
 
38
32
  class ReceiptSearch < SearchLingo::AbstractSearch # :nodoc:
39
- parser Parsers::IdParser.new Receipt.table_name
40
-
41
- parser SearchLingo::Parsers::DateParser.new Receipt.table_name,
42
- :check_date
43
- parser SearchLingo::Parsers::DateRangeParser.new Receipt.table_name,
44
- :check_date
45
- parser SearchLingo::Parsers::OpenDateRangeParser.new Receipt.table_name,
46
- :check_date, connection: Receipt.connection
47
-
48
- parser SearchLingo::Parsers::DateParser.new Receipt.table_name,
49
- :post_date, 'posted'
50
- parser SearchLingo::Parsers::DateRangeParser.new Receipt.table_name,
51
- :post_date, 'posted'
52
- parser SearchLingo::Parsers::OpenDateRangeParser.new Receipt.table_name,
53
- :post_date, 'posted', connection: Receipt.connection
33
+ parser SearchLingo::Parsers::DateParser.new Receipt.arel_table[:check_date]
34
+ parser SearchLingo::Parsers::DateParser.new Receipt.arel_table[:post_date],
35
+ modifier: 'posted'
54
36
 
55
37
  parser do |token|
56
38
  token.match /\Aamount: (\d+(?:\.\d+)?)\z/ do |m|
@@ -59,19 +41,19 @@ class ReceiptSearch < SearchLingo::AbstractSearch # :nodoc:
59
41
  end
60
42
 
61
43
  def default_parse(token)
62
- [:where, 'receipts.check_no LIKE ?', token]
44
+ [:where, Receipt.arel_table[:check_no].like(token)]
63
45
  end
64
46
  end
65
47
 
66
48
  search = JobSearch.new('6/4/15-6/5/15 id: 42 "foo bar"')
67
- search.results # => Job
68
- # .where('jobs' => { date: Date.new(2015, 6, 4)..Date.new(2015, 6, 5) })
69
- # .where('jobs' => { id: '42' })
70
- # .where('jobs.name LIKE ?', '%foo bar%')
49
+ search.results
50
+ # => Job.where(Job.arel_table[:date].in(Date.new(2015,6,4)..Date.new(2015,6,5)))
51
+ # .where('jobs' => { id: '42' })
52
+ # .where(Job.arel_table[:name].lower.like('%foo bar%'))
71
53
 
72
54
  search = ReceiptSearch.new('-6/4/15 posted: 6/5/15- amount: 1000 123')
73
- search.results # => Receipt
74
- # .where('"receipts"."check_date" <= ?', Date.new(2015, 6, 4))
75
- # .where('"receipts"."post_date" >= ?', Date.new(2015, 6, 5))
76
- # .where(receipts: { amount: '1000' })
77
- # .where('receipts.check_no LIKE ?', 123)
55
+ search.results
56
+ # => Receipt.where(Receipt.arel_table[:check_date].lteq(Date.new(2015,6,4)))
57
+ # .where(Receipt.arel_table[:post_date].gteq(Date.new(2015,6,5)))
58
+ # .where(receipts: { amount: '1000' })
59
+ # .where(Receipt.arel_table[:check_no].matches('123'))
@@ -31,7 +31,7 @@ end
31
31
  class CategoryParser # :nodoc:
32
32
  def call(token)
33
33
  if token.modifier == 'cat'
34
- [:where, { category__name: token.term }]
34
+ [:where, { Sequel.qualify('category', 'name') => token.term }]
35
35
  end
36
36
  end
37
37
  end
@@ -39,12 +39,20 @@ end
39
39
  class TaskSearch < SearchLingo::AbstractSearch # :nodoc:
40
40
  parser CategoryParser.new
41
41
 
42
+ # Match categories with priority less than or greater than a given value.
43
+ #
44
+ # <2 => Categories with priority < 2
45
+ # >5 => Categories with priority > 5
42
46
  parser do |token|
43
47
  token.match /\A([<>])([[:digit:]]+)\z/ do |m|
44
- [:where, ->{ priority.send m[1], m[2] }]
48
+ [:where, Sequel.expr { priority.send m[1], m[2] }]
45
49
  end
46
50
  end
47
51
 
52
+ # Match tasks with a given due_date.
53
+ #
54
+ # 7/4/1776 => Tasks with due_date == Date.new(1776, 7, 4)
55
+ # 7/4/17 => Tasks with due_date == Date.new(2017, 7, 4)
48
56
  parser do |token|
49
57
  token.match %r{\A(?<m>\d{1,2})/(?<d>\d{1,2})/(?<y>\d{2}\d{2}?)\z} do |m|
50
58
  begin
@@ -54,8 +62,12 @@ class TaskSearch < SearchLingo::AbstractSearch # :nodoc:
54
62
  end
55
63
  end
56
64
 
65
+ # Match tasks with names that contain a given term.
66
+ #
67
+ # pay bills => Match tasks with names like "pay bills", "pay bills by today"
68
+ # brush teeth => Match tasks with names like "brush teeth", "brush teeth and floss"
57
69
  def default_parse(token)
58
- [:where, 'tasks.name LIKE ?', "%#{token.term}%"]
70
+ [:where, Sequel.lit('tasks.name LIKE ?', "%#{token.term}%")]
59
71
  end
60
72
 
61
73
  def scope
@@ -23,34 +23,30 @@ module SearchLingo
23
23
  ##
24
24
  # Adds a new parser to the list of parsers used by this class.
25
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.
26
+ # The parser may be given as an anonymous block or as any argument which
27
+ # responds to +#call+. The parser will be send +#call+ with a single
28
+ # argument which will be a token from the query string.
29
+ #
30
+ # If both a callable object and a block are given, or if neither a callable
31
+ # object nor a block are given, an +ArgumentError+ will be raised.
31
32
  #
32
33
  # class MyParser
33
- # def call
34
+ # def call(token)
34
35
  # # return something
35
36
  # end
36
37
  # end
37
38
  #
38
39
  # class MySearch < SearchLingo::AbstractSearch
39
40
  # parser MyParser.new
40
- # parser do
41
+ # parser do |token|
41
42
  # # return something
42
43
  # end
43
44
  # end
44
- def self.parser(callable = nil, &block)
45
- unless callable || block_given?
46
- raise ArgumentError, 'parse must be called with callable or block'
47
- end
48
- if callable && block_given?
49
- # TODO: should this raise an error instead?
50
- warn "WARNING: parse called with callable and block (#{caller.first}"
45
+ def self.parser(parser = nil, &block)
46
+ unless block_given? ^ parser.respond_to?(:call)
47
+ raise ArgumentError, 'parse must be called with callable OR block'
51
48
  end
52
-
53
- parsers << (callable || block)
49
+ parsers << (parser || block)
54
50
  end
55
51
 
56
52
  ##
@@ -62,7 +58,13 @@ module SearchLingo
62
58
  ##
63
59
  # Returns the results of executing the search.
64
60
  def results
65
- @results ||= conditions.inject(scope) do |query, condition|
61
+ @results ||= load_results
62
+ end
63
+
64
+ ##
65
+ # Constructs and performs the query.
66
+ def load_results
67
+ conditions.inject(scope) do |query, condition|
66
68
  query.public_send(*condition)
67
69
  end
68
70
  end
@@ -85,11 +87,16 @@ module SearchLingo
85
87
  def conditions
86
88
  tokenizer.inject([]) do |conditions, token|
87
89
  conditions << catch(:match) do
90
+ # 1. Try each parser with the token until :match is thrown.
88
91
  parse token
92
+
93
+ # 2. If :match not thrown and token is compound, simplify and try again.
89
94
  if token.compound?
90
95
  token = tokenizer.simplify
91
96
  parse token
92
97
  end
98
+
99
+ # 3. If :match still not thrown, fallback on default parser.
93
100
  default_parse token
94
101
  end
95
102
  end
@@ -113,6 +120,7 @@ module SearchLingo
113
120
  result = parser.call token
114
121
  throw :match, result if result
115
122
  end
123
+ nil
116
124
  end
117
125
 
118
126
  ##
@@ -5,33 +5,71 @@ module SearchLingo
5
5
  class DateParser
6
6
  include MDY
7
7
 
8
- def initialize(table, column, modifier = nil, **options)
9
- @table = table
10
- @column = column
11
- @prefix = %r{#{modifier}:\s*} if modifier
12
-
13
- post_initialize **options
8
+ ##
9
+ # Instantiates a new DateParser object.
10
+ #
11
+ # The required argument +column+ should be an Arel attribute.
12
+ #
13
+ # If present, the optional argument +modifier+ will be used as the
14
+ # operator which precedes the date term.
15
+ #
16
+ # DateParser.new Booking.arel_table[:date]
17
+ # DateParser.new Contract.arel_table[:date], modifier: 'contract'
18
+ def initialize(column, modifier: nil)
19
+ @column = column
20
+ @prefix = %r{#{modifier}:[[:space:]]*} if modifier
14
21
  end
15
22
 
16
- attr_reader :table, :column, :prefix
23
+ attr_reader :column, :prefix
17
24
 
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.
25
+ ##
26
+ # Attempts to parse the token as a date, closed date range, or open date
27
+ # range.
28
+ #
29
+ # Examples of single dates are 7/14, 7/14/17, and 7/14/2017.
30
+ # Examples of closed date ranges are 1/1-6/30 and 7/1/16-6/30/18.
31
+ # Examples of open date ranges are -6/30 and 7/1/17-.
22
32
  def call(token)
33
+ parse_single_date(token) ||
34
+ parse_date_range(token) ||
35
+ parse_lte_date(token) ||
36
+ parse_gte_date(token)
37
+ end
38
+
39
+ def inspect # :nodoc:
40
+ '#<%s:0x%x @prefix=%s @column=%s>' %
41
+ [self.class, object_id << 1, prefix.inspect, column.inspect]
42
+ end
43
+
44
+ private
45
+
46
+ def parse_single_date(token)
23
47
  token.match /\A#{prefix}(?<date>#{US_DATE})\z/ do |m|
24
- date = parse m[:date]
25
- [:where, { table => { column => date } }] if date
48
+ date = parse(m[:date]) or return nil
49
+ [:where, column.eq(date)]
26
50
  end
27
51
  end
28
52
 
29
- def post_initialize(**) # :nodoc:
53
+ def parse_date_range(token)
54
+ token.match /\A#{prefix}(?<min>#{US_DATE})-(?<max>#{US_DATE})\z/ do |m|
55
+ min = parse(m[:min]) or return nil
56
+ max = parse(m[:max], relative_to: min.next_year) or return nil
57
+ [:where, column.in(min..max)]
58
+ end
30
59
  end
31
60
 
32
- def inspect # :nodoc:
33
- '#<%s:0x%x @table=%s @column=%s @prefix=%s>' %
34
- [self.class, object_id << 1, table.inspect, column.inspect, prefix.inspect]
61
+ def parse_lte_date(token)
62
+ token.match /\A#{prefix}-(?<date>#{US_DATE})\z/ do |m|
63
+ date = parse(m[:date]) or return nil
64
+ [:where, column.lteq(date)]
65
+ end
66
+ end
67
+
68
+ def parse_gte_date(token)
69
+ token.match /\A#{prefix}(?<date>#{US_DATE})-\z/ do |m|
70
+ date = parse(m[:date]) or return nil
71
+ [:where, column.gteq(date)]
72
+ end
35
73
  end
36
74
  end
37
75
  end
@@ -13,8 +13,8 @@ module SearchLingo
13
13
  # Returns a +Date+ object for the date represented by +term+. Returns
14
14
  # +nil+ if +term+ can not be parsed.
15
15
  #
16
- # If the year has two digits, it will be expanded into a four-digit by
17
- # +Date.parse+.
16
+ # If the year has two digits, it will be implicitly expanded into a
17
+ # four-digit year by +Date.parse+. Otherwise it will be used as is.
18
18
  #
19
19
  # If the year is omitted, it will be inferred using +relative_to+ as a
20
20
  # reference date. In this scenario, the resulting date will always be
@@ -26,12 +26,13 @@ module SearchLingo
26
26
  term.match /\A#{US_DATE}\z/ do |m|
27
27
  return Date.parse "#{m[:y]}/#{m[:m]}/#{m[:d]}" if m[:y]
28
28
 
29
+ ref = relative_to
29
30
  day = Integer(m[:d])
30
31
  month = Integer(m[:m])
31
- year = if month < relative_to.month || month == relative_to.month && day <= relative_to.day
32
- relative_to.year
32
+ year = if month < ref.month || month == ref.month && day <= ref.day
33
+ ref.year
33
34
  else
34
- relative_to.year - 1
35
+ ref.year - 1
35
36
  end
36
37
 
37
38
  Date.new year, month, day
@@ -1,6 +1 @@
1
1
  require 'search_lingo/parsers/date_parser'
2
- require 'search_lingo/parsers/date_range_parser'
3
- require 'search_lingo/parsers/open_date_range_parser'
4
-
5
- require 'search_lingo/parsers/lte_date_parser'
6
- require 'search_lingo/parsers/gte_date_parser'
@@ -1,3 +1,3 @@
1
1
  module SearchLingo
2
- VERSION = '1.0.2'
2
+ VERSION = '1.0.3'
3
3
  end
data/search_lingo.gemspec CHANGED
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "rake", "~> 10.0"
28
28
  spec.add_development_dependency 'minitest'
29
29
  spec.add_development_dependency 'pry'
30
- spec.add_development_dependency 'sequel'
30
+ spec.add_development_dependency 'sequel', '~> 4.48'
31
31
  spec.add_development_dependency 'sqlite3'
32
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.2
4
+ version: 1.0.3
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-07-07 00:00:00.000000000 Z
11
+ date: 2018-10-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -70,16 +70,16 @@ dependencies:
70
70
  name: sequel
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ">="
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '4.48'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ">="
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '4.48'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: sqlite3
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -117,11 +117,7 @@ files:
117
117
  - lib/search_lingo/constants.rb
118
118
  - lib/search_lingo/parsers.rb
119
119
  - lib/search_lingo/parsers/date_parser.rb
120
- - lib/search_lingo/parsers/date_range_parser.rb
121
- - lib/search_lingo/parsers/gte_date_parser.rb
122
- - lib/search_lingo/parsers/lte_date_parser.rb
123
120
  - lib/search_lingo/parsers/mdy.rb
124
- - lib/search_lingo/parsers/open_date_range_parser.rb
125
121
  - lib/search_lingo/token.rb
126
122
  - lib/search_lingo/tokenizer.rb
127
123
  - lib/search_lingo/version.rb
@@ -150,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
150
146
  version: '0'
151
147
  requirements: []
152
148
  rubyforge_project:
153
- rubygems_version: 2.4.8
149
+ rubygems_version: 2.7.7
154
150
  signing_key:
155
151
  specification_version: 4
156
152
  summary: Framework for building simple query languages
@@ -1,15 +0,0 @@
1
- require 'search_lingo/parsers/date_parser'
2
-
3
- module SearchLingo
4
- module Parsers # :nodoc:
5
- class DateRangeParser < DateParser
6
- def call(token)
7
- token.match /\A#{prefix}(?<min>#{US_DATE})-(?<max>#{US_DATE})\z/ do |m|
8
- min = parse m[:min]
9
- max = parse m[:max], relative_to: min.next_year if min
10
- [:where, { table => { column => min..max } }] if min && max
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,13 +0,0 @@
1
- require 'search_lingo/parsers/open_date_range_parser'
2
-
3
- module SearchLingo
4
- module Parsers # :nodoc:
5
- class GTEDateParser < OpenDateRangeParser # :nodoc:
6
- def initialize(*)
7
- warn "DEPRECATION WARNING: use SearchLingo::Parsers::OpenDateRangeParser " \
8
- "instead of #{self.class} (from #{caller.first})"
9
- super
10
- end
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- require 'search_lingo/parsers/open_date_range_parser'
2
-
3
- module SearchLingo
4
- module Parsers # :nodoc:
5
- class LTEDateParser < OpenDateRangeParser # :nodoc:
6
- def initialize(*)
7
- warn "DEPRECATION WARNING: use SearchLingo::Parsers::OpenDateRangeParser " \
8
- "instead of #{self.class} (from #{caller.first})"
9
- super
10
- end
11
- end
12
- end
13
- end
@@ -1,46 +0,0 @@
1
- require 'search_lingo/parsers/date_parser'
2
- require 'forwardable'
3
-
4
- module SearchLingo
5
- module Parsers # :nodoc:
6
- class OpenDateRangeParser < DateParser
7
- extend Forwardable
8
-
9
- def call(token)
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])
24
- [
25
- :where,
26
- "#{quote_table_name table}.#{quote_column_name column} <= ?",
27
- date
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])
36
- [
37
- :where,
38
- "#{quote_table_name table}.#{quote_column_name column} >= ?",
39
- date
40
- ]
41
- end
42
- end
43
- end
44
- end
45
- end
46
- end