search_lingo 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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