search_lingo 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/complex.rb +83 -0
- data/examples/simple.rb +31 -0
- data/lib/search_lingo/abstract_search.rb +63 -0
- data/lib/search_lingo/parsers/date_parser.rb +34 -0
- data/lib/search_lingo/parsers/date_range_parser.rb +15 -0
- data/lib/search_lingo/parsers/gte_date_parser.rb +25 -0
- data/lib/search_lingo/parsers/lte_date_parser.rb +25 -0
- data/lib/search_lingo/parsers/mdy.rb +30 -0
- data/lib/search_lingo/token.rb +23 -0
- data/lib/search_lingo/tokenizer.rb +44 -0
- data/lib/search_lingo/version.rb +3 -0
- data/lib/search_lingo.rb +7 -0
- data/search_lingo.gemspec +27 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3dfc725892e1b65db47327257cd8c082536dd74b
|
4
|
+
data.tar.gz: 78e4297906559efcb5cedc54b003cf2abfaf4ac1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5359eff62098245796f55f5abe4d3e663e0e56677b7925ba1267bf0e00dda71cfefa84d87c416a57a6b7f21699ca7b2b9914d96b74f37b02fa0597a35c32b931
|
7
|
+
data.tar.gz: aaa8f3414ebf996194ce415e0270fa2321290900ac98092ea34d2dcb43608e3f6df4e62146fea960814063f753ca7b05fd9fb8a3637399dd1459fe786c7700aa
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 John Parker
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# SearchLingo
|
2
|
+
|
3
|
+
SearchLingo is a framework for defining simple, user-friendly query languages
|
4
|
+
and translating them into their underlying queries.
|
5
|
+
|
6
|
+
It was originally designed after I found myself implementing the same basic
|
7
|
+
query parsing over and over again across different projects. I wanted a way to
|
8
|
+
simplify the process without having to worry about application-specific aspects
|
9
|
+
of searching.
|
10
|
+
|
11
|
+
The way the searches themselves are performed lies outside the scope of this
|
12
|
+
project. Although originally designed to work with basic searching with
|
13
|
+
ActiveRecord models, it should be usable with other data stores provided they
|
14
|
+
let you chain queries together onto a single object.
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Add this line to your application's Gemfile:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'search_lingo'
|
22
|
+
```
|
23
|
+
|
24
|
+
And then execute:
|
25
|
+
|
26
|
+
$ bundle
|
27
|
+
|
28
|
+
Or install it yourself as:
|
29
|
+
|
30
|
+
$ gem install search_lingo
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
Create a class which inherits from SearchLingo::AbstractSearch. Provide an
|
35
|
+
implementation of <code>#default_parse</code> in that class. Register parsers
|
36
|
+
for specific types of search tokens using the <code>parser</code> class method.
|
37
|
+
|
38
|
+
Instantiate your search class by passing in the query string and the scope on
|
39
|
+
which to perform the search. Use the <code>#results</code> method to compile
|
40
|
+
the search and return the results.
|
41
|
+
|
42
|
+
Take a look at the examples/ directory for more concrete examples.
|
43
|
+
|
44
|
+
## How It Works
|
45
|
+
|
46
|
+
A search is instantiated with a query string and a search scope (commonly an
|
47
|
+
ActiveRecord model). The search breaks the query string down into a series of
|
48
|
+
tokens, and each token is processed by a declared series of parsers. If a
|
49
|
+
parser succeeds, the process immediately terminates and advances to the next
|
50
|
+
token. If none of the declared parsers succeeds, and the token is compound --
|
51
|
+
that is, the token is composed of an operator and a term (e.g., "foo: bar"),
|
52
|
+
the token is simplified and then processed by the declared parsers again. If
|
53
|
+
the second pass also fails, then the (now simplified) token falls through to
|
54
|
+
the <code>#default_parse</code> method defined by the search class. (It is
|
55
|
+
important that this method be implemented in such a way that it always
|
56
|
+
succeeds.)
|
57
|
+
|
58
|
+
## Search Classes
|
59
|
+
|
60
|
+
Search classes should inherit from SearchLingo::AbstractSearch and they should
|
61
|
+
override the <code>#default_parse</code> instance method. It is important that
|
62
|
+
this method be defined in such a way that it always succeeds, as the results
|
63
|
+
will be sent to the query object via <code>#public_send</code>. In addtion, the
|
64
|
+
class method <code>parser</code> can be used to declare additional parsers that
|
65
|
+
should be used by the search class. (See the section "Parsing" for more
|
66
|
+
information on what makes a suitable parser.)
|
67
|
+
|
68
|
+
## Parsers
|
69
|
+
|
70
|
+
Any object that can respond to the <code>#call</code> method can be used as a
|
71
|
+
parser. If the parser succeeds, it should return an Array of arguments that can
|
72
|
+
be sent to the query object using <code>#public_send</code>, e.g.,
|
73
|
+
<code>[:where, { id: 42 }]</code>. If the parser fails, it should return a
|
74
|
+
falsey value (typically nil).
|
75
|
+
|
76
|
+
For very simple parsers which need not be reusable, you can pass the
|
77
|
+
parsing logic to the <code>parser</code> method as a block:
|
78
|
+
|
79
|
+
class MySearch < SearchLingo::AbstractSearch
|
80
|
+
parser do |token|
|
81
|
+
token.match /\Aid:[[:space:]]*([[:digit:]]+)\z/ do |m|
|
82
|
+
[:where, { id: m[1] }]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Parsers can also be implemented as lambdas:
|
88
|
+
|
89
|
+
module Parsers
|
90
|
+
ID_PARSER = lambda do |token|
|
91
|
+
token.match h/\Aid:[[:space:]]*([[:digit:]]+)\z/ do |m|
|
92
|
+
[:where, { id: m[1] }]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class MySearch < SearchLingo::AbstractSearch
|
98
|
+
parser Parsers::ID_PARSER
|
99
|
+
end
|
100
|
+
|
101
|
+
class MyOtherSearch < SearchLingo::AbstractSearch
|
102
|
+
parser Parsers::ID_PARSER
|
103
|
+
end
|
104
|
+
|
105
|
+
Finally, for the most complicated cases, you could implement parsers as
|
106
|
+
classes:
|
107
|
+
|
108
|
+
module Parsers
|
109
|
+
class IdParser
|
110
|
+
def initialize(table, operator = nil)
|
111
|
+
@table = table
|
112
|
+
@prefix = /#{operator}:\s*/ if operator
|
113
|
+
end
|
114
|
+
|
115
|
+
def call(token)
|
116
|
+
token.match /\A#{@prefix}([[:digit:]]+)\z/ do |m|
|
117
|
+
[:where, { @table => { id: m[1] } }]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class EventSearch < SearchLingo::AbstractSearch
|
124
|
+
parser Parsers::IdParser.new :events # => match "42"
|
125
|
+
parser Parsers::IdParser.new :categories, 'category' # => match "category: 42"
|
126
|
+
end
|
127
|
+
|
128
|
+
class CategorySearch < SearchLingo::AbstractSearch
|
129
|
+
parser Parsers::IdParser.new :categories
|
130
|
+
end
|
131
|
+
|
132
|
+
## Tokenization
|
133
|
+
|
134
|
+
Queries are comprised of one or more tokens separated by spaces. A simple token
|
135
|
+
is a term which can be a single word (or date, number, etc.) or multiple terms
|
136
|
+
within a pair of double quotes. A compound token is a simple token preceded by
|
137
|
+
an operator followed by zero or more spaces.
|
138
|
+
|
139
|
+
QUERY := TOKEN*
|
140
|
+
TOKEN := COMPOUND_TOKEN | TERM
|
141
|
+
COMPOUND_TOKEN := OPERATOR TERM
|
142
|
+
OPERATOR := [[:graph:]]+:
|
143
|
+
TERM := "[^"]*" | [[:graph:]]+
|
144
|
+
|
145
|
+
Terms can be things like:
|
146
|
+
|
147
|
+
* foo
|
148
|
+
* "foo bar"
|
149
|
+
* 6/14/15
|
150
|
+
* 1000.00
|
151
|
+
|
152
|
+
Operators can be things like:
|
153
|
+
|
154
|
+
* foo:
|
155
|
+
* bar_baz:
|
156
|
+
|
157
|
+
(If you want to perform a query with a term that could potentially be parsed as
|
158
|
+
an operator, you would place the term in quotes, i.e., "foo:".)
|
159
|
+
|
160
|
+
## Development
|
161
|
+
|
162
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
163
|
+
`bin/console` for an interactive prompt that will allow you to experiment.
|
164
|
+
|
165
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
166
|
+
release a new version, update the version number in `version.rb`, and then run
|
167
|
+
`bundle exec rake release` to create a git tag for the version, push git
|
168
|
+
commits and tags, and push the `.gem` file to
|
169
|
+
[rubygems.org](https://rubygems.org).
|
170
|
+
|
171
|
+
## Contributing
|
172
|
+
|
173
|
+
1. Fork it ( https://github.com/jparker/search_lingo/fork )
|
174
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
175
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
176
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
177
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "search_lingo"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start
|
data/bin/setup
ADDED
data/examples/complex.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class Job < ActiveRecord::Base
|
2
|
+
# Assume this model has attributes: :id, :date, :name
|
3
|
+
end
|
4
|
+
|
5
|
+
class Receipt < ActiveRecord::Base
|
6
|
+
# Assume this model has attributes: :id, :check_no, :check_date, :post_date, :amount
|
7
|
+
end
|
8
|
+
|
9
|
+
module Parsers
|
10
|
+
class IdParser
|
11
|
+
def initialize(table)
|
12
|
+
@table = table
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(token)
|
16
|
+
token.match /\Aid:\s*([[:digit:]]+)\z/ do |m|
|
17
|
+
[:where, { @table => { id: m[1] } }]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class JobSearch < AbstractSearch
|
24
|
+
parser Parsers::IdParser.new Job.table_name
|
25
|
+
|
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::LTEDateParser.new Job.table_name,
|
31
|
+
:date, connection: Job.connection
|
32
|
+
parser SearchLingo::Parsers::GTEDateParser.new Job.table_name,
|
33
|
+
:date, connection: Job.connection
|
34
|
+
|
35
|
+
def default_parse(token)
|
36
|
+
[:where, 'jobs.name LIKE ?', "%#{token}%"]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class ReceiptSearch < AbstractSearch
|
41
|
+
parser Parsers::IdParser.new Receipt.table_name
|
42
|
+
|
43
|
+
parser SearchLingo::Parsers::DateParser.new Receipt.table_name,
|
44
|
+
:check_date
|
45
|
+
parser SearchLingo::Parsers::DateRangeParser.new Receipt.table_name,
|
46
|
+
:check_date
|
47
|
+
parser SearchLingo::Parsers::LTEDateParser.new Receipt.table_name,
|
48
|
+
:check_date, connection: Receipt.connection
|
49
|
+
parser SearchLingo::Parsers::GTEDateParser.new Receipt.table_name,
|
50
|
+
:check_date, connection: Receipt.connection
|
51
|
+
|
52
|
+
parser SearchLingo::Parsers::DateParser.new Receipt.table_name,
|
53
|
+
:post_date, 'posted'
|
54
|
+
parser SearchLingo::Parsers::DateRangeParser.new Receipt.table_name,
|
55
|
+
:post_date, 'posted'
|
56
|
+
parser SearchLingo::Parsers::LTEDateParser.new Receipt.table_name,
|
57
|
+
:post_date, 'posted', connection: Receipt.connection
|
58
|
+
parser SearchLingo::Parsers::GTEDateParser.new Receipt.table_name,
|
59
|
+
:post_date, 'posted', connection: Receipt.connection
|
60
|
+
|
61
|
+
parser do |token|
|
62
|
+
token.match /\Aamount: (\d+(?:\.\d+)?)\z/ do |m|
|
63
|
+
[:where, { receipts: { amount: m[1] } }]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def default_parse(token)
|
68
|
+
[:where, 'receipts.check_no LIKE ?', token]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
search = JobSearch.new('6/4/15-6/5/15 id: 42 "foo bar"')
|
73
|
+
search.results # => Job
|
74
|
+
# .where('jobs' => { date: Date.new(2015, 6, 4)..Date.new(2015, 6, 5) })
|
75
|
+
# .where('jobs' => { id: '42' })
|
76
|
+
# .where('jobs.name LIKE ?', '%foo bar%')
|
77
|
+
|
78
|
+
search = ReceiptSearch.new('-6/4/15 posted: 6/5/15- amount: 1000 123')
|
79
|
+
search.results # => Receipt
|
80
|
+
# .where('"receipts"."check_date" <= ?', Date.new(2015, 6, 4))
|
81
|
+
# .where('"receipts"."post_date" >= ?', Date.new(2015, 6, 5))
|
82
|
+
# .where(receipts: { amount: '1000' })
|
83
|
+
# .where('receipts.check_no LIKE ?', 123)
|
data/examples/simple.rb
ADDED
@@ -0,0 +1,31 @@
|
|
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%')
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'search_lingo/tokenizer'
|
2
|
+
|
3
|
+
module SearchLingo
|
4
|
+
class AbstractSearch
|
5
|
+
def initialize(query, scope, tokenizer: Tokenizer)
|
6
|
+
@query = query || ''
|
7
|
+
@scope = scope
|
8
|
+
@tokenizer = tokenizer.new @query
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :query, :scope, :tokenizer
|
12
|
+
|
13
|
+
def self.parsers
|
14
|
+
@parsers ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parser(callable = nil, &block)
|
18
|
+
unless callable || block_given?
|
19
|
+
raise ArgumentError, '.parse must be called with callable or block'
|
20
|
+
end
|
21
|
+
if callable && block_given?
|
22
|
+
warn "WARNING: parse called with callable and block (#{caller.first}"
|
23
|
+
end
|
24
|
+
|
25
|
+
parsers << (callable || block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def parsers
|
29
|
+
self.class.parsers
|
30
|
+
end
|
31
|
+
|
32
|
+
def results
|
33
|
+
@results ||= conditions.inject(scope) do |query, condition|
|
34
|
+
query.public_send(*condition)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def conditions
|
39
|
+
tokenizer.inject([]) do |conditions, token|
|
40
|
+
conditions << catch(:match) do
|
41
|
+
parse token
|
42
|
+
if token.compound?
|
43
|
+
token = tokenizer.simplify
|
44
|
+
parse token
|
45
|
+
end
|
46
|
+
default_parse token
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def parse(token)
|
52
|
+
parsers.each do |parser|
|
53
|
+
result = parser.call token
|
54
|
+
throw :match, result if result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_parse(token)
|
59
|
+
raise NotImplementedError,
|
60
|
+
"#default_parse must be implemented by #{self.class}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'search_lingo/parsers/mdy'
|
2
|
+
|
3
|
+
module SearchLingo
|
4
|
+
module Parsers
|
5
|
+
class DateParser
|
6
|
+
include MDY
|
7
|
+
|
8
|
+
def initialize(table, column, operator = nil, **options)
|
9
|
+
@table = table
|
10
|
+
@column = column
|
11
|
+
@prefix = %r{#{operator}:\s*} if operator
|
12
|
+
|
13
|
+
post_initialize **options
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :table, :column, :prefix
|
17
|
+
|
18
|
+
def call(token)
|
19
|
+
token.match /\A#{prefix}(?<date>#{US_DATE})\z/ do |m|
|
20
|
+
date = parse m[:date]
|
21
|
+
[:where, { table => { column => date } }] if date
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def post_initialize(**)
|
26
|
+
end
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
'#<%s:0x%x @table=%s @column=%s @prefix=%s>' %
|
30
|
+
[self.class, object_id << 1, table.inspect, column.inspect, prefix.inspect]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'search_lingo/parsers/date_parser'
|
2
|
+
|
3
|
+
module SearchLingo
|
4
|
+
module Parsers
|
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
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'search_lingo/parsers/date_parser'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module SearchLingo
|
5
|
+
module Parsers
|
6
|
+
class GTEDateParser < DateParser
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def call(token)
|
10
|
+
token.match /\A#{prefix}(?<date>#{US_DATE})-\z/ do |m|
|
11
|
+
date = parse m[:date]
|
12
|
+
if date
|
13
|
+
[:where, "#{quote_table_name table}.#{quote_column_name column} >= ?", date]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_initialize(connection:, **)
|
19
|
+
@connection = connection
|
20
|
+
end
|
21
|
+
|
22
|
+
def_delegators :@connection, :quote_column_name, :quote_table_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'search_lingo/parsers/date_parser'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module SearchLingo
|
5
|
+
module Parsers
|
6
|
+
class LTEDateParser < DateParser
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def call(token)
|
10
|
+
token.match /\A#{prefix}-(?<date>#{US_DATE})\z/ do |m|
|
11
|
+
date = parse m[:date]
|
12
|
+
if date
|
13
|
+
[:where, "#{quote_table_name table}.#{quote_column_name column} <= ?", date]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_initialize(connection:, **)
|
19
|
+
@connection = connection
|
20
|
+
end
|
21
|
+
|
22
|
+
def_delegators :@connection, :quote_column_name, :quote_table_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module SearchLingo
|
4
|
+
module Parsers
|
5
|
+
module MDY
|
6
|
+
US_DATE = %r{(?<m>\d{1,2})/(?<d>\d{1,2})(?:/(?<y>\d{2}\d{2}?))?}
|
7
|
+
|
8
|
+
def parse(term, relative_to: Date.today)
|
9
|
+
term.match /\A#{US_DATE}\z/ do |m|
|
10
|
+
return Date.parse "#{m[:y]}/#{m[:m]}/#{m[:d]}" if m[:y]
|
11
|
+
|
12
|
+
day = Integer(m[:d])
|
13
|
+
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
|
20
|
+
end
|
21
|
+
|
22
|
+
Date.new year, month, day
|
23
|
+
end
|
24
|
+
rescue ArgumentError
|
25
|
+
end
|
26
|
+
|
27
|
+
module_function :parse
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module SearchLingo
|
4
|
+
class Token < DelegateClass(String)
|
5
|
+
FORMAT = %r{\A(?:(\S+):\s*)?"?(.+?)"?\z}
|
6
|
+
|
7
|
+
def operator
|
8
|
+
self[FORMAT, 1]
|
9
|
+
end
|
10
|
+
|
11
|
+
def term
|
12
|
+
self[FORMAT, 2]
|
13
|
+
end
|
14
|
+
|
15
|
+
def compound?
|
16
|
+
!!operator
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
'#<%s %s operator=%s term=%s>' % [self.class, super, operator.inspect, term.inspect]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'strscan'
|
3
|
+
require 'search_lingo/token'
|
4
|
+
|
5
|
+
module SearchLingo
|
6
|
+
class Tokenizer
|
7
|
+
include Enumerable
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
SIMPLE = %r{"[^"]*"|[[:graph:]]+}
|
11
|
+
COMPOUND = %r{(?:[[:graph:]]+:[[:space:]]*)?#{SIMPLE}}
|
12
|
+
DELIMITER = %r{[[:space:]]*}
|
13
|
+
|
14
|
+
def initialize(query)
|
15
|
+
@scanner = StringScanner.new query.strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def enum
|
19
|
+
Enumerator.new do |yielder|
|
20
|
+
until scanner.eos?
|
21
|
+
token = scanner.scan COMPOUND
|
22
|
+
if token
|
23
|
+
yielder << Token.new(token)
|
24
|
+
end
|
25
|
+
scanner.skip DELIMITER
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def_delegator :scanner, :reset
|
31
|
+
def_delegators :enum, :each, :next
|
32
|
+
|
33
|
+
def simplify
|
34
|
+
scanner.unscan
|
35
|
+
Token.new(scanner.scan(SIMPLE)).tap do
|
36
|
+
scanner.skip DELIMITER
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :scanner
|
43
|
+
end
|
44
|
+
end
|
data/lib/search_lingo.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'search_lingo/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "search_lingo"
|
8
|
+
spec.version = SearchLingo::VERSION
|
9
|
+
spec.authors = ["John Parker"]
|
10
|
+
spec.email = ["jparker@urgetopunt.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Framework for building simple query languages}
|
13
|
+
spec.description = %q{Framework for building simple query language for converting user queries into database queries.}
|
14
|
+
spec.homepage = "https://github.com/jparker/search_lingo"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency 'minitest'
|
25
|
+
spec.add_development_dependency 'minitest-focus'
|
26
|
+
spec.add_development_dependency 'pry'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: search_lingo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.beta2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Parker
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-06-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-focus
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
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
|
+
description: Framework for building simple query language for converting user queries
|
84
|
+
into database queries.
|
85
|
+
email:
|
86
|
+
- jparker@urgetopunt.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/setup
|
99
|
+
- examples/complex.rb
|
100
|
+
- examples/simple.rb
|
101
|
+
- lib/search_lingo.rb
|
102
|
+
- lib/search_lingo/abstract_search.rb
|
103
|
+
- lib/search_lingo/parsers/date_parser.rb
|
104
|
+
- lib/search_lingo/parsers/date_range_parser.rb
|
105
|
+
- lib/search_lingo/parsers/gte_date_parser.rb
|
106
|
+
- lib/search_lingo/parsers/lte_date_parser.rb
|
107
|
+
- lib/search_lingo/parsers/mdy.rb
|
108
|
+
- lib/search_lingo/token.rb
|
109
|
+
- lib/search_lingo/tokenizer.rb
|
110
|
+
- lib/search_lingo/version.rb
|
111
|
+
- search_lingo.gemspec
|
112
|
+
homepage: https://github.com/jparker/search_lingo
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
metadata: {}
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - ">"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: 1.3.1
|
130
|
+
requirements: []
|
131
|
+
rubyforge_project:
|
132
|
+
rubygems_version: 2.4.7
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: Framework for building simple query languages
|
136
|
+
test_files: []
|