search_lingo 1.0.0.beta2 → 1.0.0.beta3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +89 -36
- data/lib/search_lingo/abstract_search.rb +7 -4
- data/lib/search_lingo/constants.rb +4 -0
- data/lib/search_lingo/parsers.rb +4 -0
- data/lib/search_lingo/token.rb +6 -4
- data/lib/search_lingo/tokenizer.rb +17 -16
- data/lib/search_lingo/version.rb +1 -1
- data/lib/search_lingo.rb +1 -5
- data/search_lingo.gemspec +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5f1e9e130f36eb2a7481db8481a510606e0f250d
|
4
|
+
data.tar.gz: 9add0798376d6e52412102eb0ba76c542f43ca88
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d299c1e7b9fac6aeb2d17ccaaeb4ecacf3afcb42e71b009c9b2da5b9f6fd1b42238e4d33c3338ed1efddea1d776f92df352ad620e68dcbafa96f83fc630305ef
|
7
|
+
data.tar.gz: 8851f6d29a00c39bdce63e8381603a6b2970e8b0804b9549d22d2510203caaef71b04530d04deab7b799da9d240ebb4235429cdedda0e1beca53346eb3ea6bb5
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -13,6 +13,9 @@ project. Although originally designed to work with basic searching with
|
|
13
13
|
ActiveRecord models, it should be usable with other data stores provided they
|
14
14
|
let you chain queries together onto a single object.
|
15
15
|
|
16
|
+
Be advised this software is still in beta release, and some of the internals
|
17
|
+
are still subject to significant change.
|
18
|
+
|
16
19
|
## Installation
|
17
20
|
|
18
21
|
Add this line to your application's Gemfile:
|
@@ -31,6 +34,51 @@ Or install it yourself as:
|
|
31
34
|
|
32
35
|
## Usage
|
33
36
|
|
37
|
+
Here is a simple example.
|
38
|
+
|
39
|
+
class Task < ActiveRecord::Base
|
40
|
+
end
|
41
|
+
|
42
|
+
class TaskSearch < SearchLingo::AbstractSearch
|
43
|
+
def default_parse(token)
|
44
|
+
[:where, 'tasks.name LIKE ?', "%#{token}%"]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
TaskSearch.new('foo bar', Task).results
|
49
|
+
# => Task.where('tasks.name LIKE ?', '%foo%').where('tasks.name LIKE ?', '%bar%')
|
50
|
+
TaskSearch.new('"foo bar"', Task).results
|
51
|
+
# => Task.where('tasks.name LIKE ?', '%foo bar%')
|
52
|
+
|
53
|
+
And here is a more complex example.
|
54
|
+
|
55
|
+
class Category < ActiveRecord::Base
|
56
|
+
has_many :tasks
|
57
|
+
end
|
58
|
+
|
59
|
+
class Task < ActiveRecord::Base
|
60
|
+
belongs_to :category
|
61
|
+
end
|
62
|
+
|
63
|
+
class TaskSearch < SearchLingo::AbstractSearch
|
64
|
+
parser do |token|
|
65
|
+
token.match /\Acategory:\s*"?(.*?)"?\z/ do |m}
|
66
|
+
[:where, { categories: { name: m[1] } }]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def default_parse(token)
|
71
|
+
[:where, 'tasks.name LIKE ?', "%#{token}%"]
|
72
|
+
end
|
73
|
+
|
74
|
+
def scope
|
75
|
+
@scope.includes(:category).references(:category)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
TaskSearch.new('category: "foo bar" baz', Task).results
|
80
|
+
# => Task.includes(:category).references(:category).where(categories: { name: 'foo bar' }).where('tasks.name LIKE ?', '%baz%')
|
81
|
+
|
34
82
|
Create a class which inherits from SearchLingo::AbstractSearch. Provide an
|
35
83
|
implementation of <code>#default_parse</code> in that class. Register parsers
|
36
84
|
for specific types of search tokens using the <code>parser</code> class method.
|
@@ -57,13 +105,46 @@ succeeds.)
|
|
57
105
|
|
58
106
|
## Search Classes
|
59
107
|
|
60
|
-
Search classes should inherit from
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
class
|
65
|
-
|
66
|
-
information on
|
108
|
+
Search classes should inherit from SearchLogic::AbstractSearch, and they must
|
109
|
+
provide their own implementation of <code>#default_parse</code>. Optionally, a
|
110
|
+
search class may also use the parse class method to add specialized parsers for
|
111
|
+
handling tokens that match specific patterns. As each token is processed, the
|
112
|
+
search class will first run through the specialized parsers. If none of them
|
113
|
+
succeed, it will fall back on the <code>#default_parse</code> method. See the
|
114
|
+
section "Parsing" for more information on how parsers work and how they should
|
115
|
+
be structured.
|
116
|
+
|
117
|
+
## Tokenization
|
118
|
+
|
119
|
+
Queries are comprised of zero or more tokens separated by white space. A token
|
120
|
+
has a term and an optional operator. (A simple token has no operator; a
|
121
|
+
compound token does.) A term can be a single word or multiple words joined by
|
122
|
+
spaces and contained within double quotes. For example <code>foo</code> and
|
123
|
+
<code>"foo bar baz"</code> are both single terms. An operator is one or more
|
124
|
+
alphanumeric characters followed by a colon and zero or more spaces.
|
125
|
+
|
126
|
+
QUERY := TOKEN*
|
127
|
+
TOKEN := (OPERATOR ':' [[:space:]]*)? TERM
|
128
|
+
OPERATOR := [[:alnum:]]+
|
129
|
+
TERM := '"' [^"]* '"' | [[:graph:]]+
|
130
|
+
|
131
|
+
The following are all examples of tokens:
|
132
|
+
|
133
|
+
* <code>foo</code>
|
134
|
+
* <code>"foo bar"</code>
|
135
|
+
* <code>foo: bar</code>
|
136
|
+
* <code>foo: "bar baz"</code>
|
137
|
+
|
138
|
+
(If you need a term to equal something that might otherwise be interpreted as
|
139
|
+
an operator, you can enclose the term in double quotes, e.g., while <code>foo:
|
140
|
+
bar</code> would be interpreted a single compound token, <code>"foo:"
|
141
|
+
bar</code> would be treated as two distinct simple tokens.)
|
142
|
+
|
143
|
+
Tokens are passed to parsers as instances of the Token class. The Token class
|
144
|
+
provides <code>#operator</code> and <code>#term</code> methods, but delegates
|
145
|
+
all other behavior to the String class. Consequently, when writing parsers, you
|
146
|
+
have the option of either interacting with the token as a raw String or making
|
147
|
+
use of the extra functionality of Tokens.
|
67
148
|
|
68
149
|
## Parsers
|
69
150
|
|
@@ -71,7 +152,7 @@ Any object that can respond to the <code>#call</code> method can be used as a
|
|
71
152
|
parser. If the parser succeeds, it should return an Array of arguments that can
|
72
153
|
be sent to the query object using <code>#public_send</code>, e.g.,
|
73
154
|
<code>[:where, { id: 42 }]</code>. If the parser fails, it should return a
|
74
|
-
falsey value
|
155
|
+
falsey value.
|
75
156
|
|
76
157
|
For very simple parsers which need not be reusable, you can pass the
|
77
158
|
parsing logic to the <code>parser</code> method as a block:
|
@@ -129,34 +210,6 @@ classes:
|
|
129
210
|
parser Parsers::IdParser.new :categories
|
130
211
|
end
|
131
212
|
|
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
213
|
## Development
|
161
214
|
|
162
215
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
@@ -2,13 +2,12 @@ require 'search_lingo/tokenizer'
|
|
2
2
|
|
3
3
|
module SearchLingo
|
4
4
|
class AbstractSearch
|
5
|
-
def initialize(query, scope
|
5
|
+
def initialize(query, scope)
|
6
6
|
@query = query || ''
|
7
7
|
@scope = scope
|
8
|
-
@tokenizer = tokenizer.new @query
|
9
8
|
end
|
10
9
|
|
11
|
-
attr_reader :query, :scope
|
10
|
+
attr_reader :query, :scope
|
12
11
|
|
13
12
|
def self.parsers
|
14
13
|
@parsers ||= []
|
@@ -16,7 +15,7 @@ module SearchLingo
|
|
16
15
|
|
17
16
|
def self.parser(callable = nil, &block)
|
18
17
|
unless callable || block_given?
|
19
|
-
raise ArgumentError, '
|
18
|
+
raise ArgumentError, 'parse must be called with callable or block'
|
20
19
|
end
|
21
20
|
if callable && block_given?
|
22
21
|
warn "WARNING: parse called with callable and block (#{caller.first}"
|
@@ -48,6 +47,10 @@ module SearchLingo
|
|
48
47
|
end
|
49
48
|
end
|
50
49
|
|
50
|
+
def tokenizer
|
51
|
+
@tokenizer ||= Tokenizer.new query
|
52
|
+
end
|
53
|
+
|
51
54
|
def parse(token)
|
52
55
|
parsers.each do |parser|
|
53
56
|
result = parser.call token
|
data/lib/search_lingo/token.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
require 'delegate'
|
2
|
+
require 'search_lingo/constants'
|
2
3
|
|
3
4
|
module SearchLingo
|
4
5
|
class Token < DelegateClass(String)
|
5
|
-
|
6
|
+
STRUCTURE = /\A(?:(#{OPERATOR}):[[:space:]]*)?"?(.+?)"?\z/
|
6
7
|
|
7
8
|
def operator
|
8
|
-
self[
|
9
|
+
self[STRUCTURE, 1]
|
9
10
|
end
|
10
11
|
|
11
12
|
def term
|
12
|
-
self[
|
13
|
+
self[STRUCTURE, 2]
|
13
14
|
end
|
14
15
|
|
15
16
|
def compound?
|
@@ -17,7 +18,8 @@ module SearchLingo
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def inspect
|
20
|
-
'#<%s %s operator=%s term=%s>' %
|
21
|
+
'#<%s String(%s) operator=%s term=%s>' %
|
22
|
+
[self.class, super, operator.inspect, term.inspect]
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'strscan'
|
3
|
+
require 'search_lingo/constants'
|
3
4
|
require 'search_lingo/token'
|
4
5
|
|
5
6
|
module SearchLingo
|
@@ -7,34 +8,34 @@ module SearchLingo
|
|
7
8
|
include Enumerable
|
8
9
|
extend Forwardable
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
DELIMITER
|
11
|
+
SIMPLE_TOKEN = /#{TERM}/
|
12
|
+
COMPOUND_TOKEN = /(?:#{OPERATOR}:[[:space:]]*)?#{TERM}/
|
13
|
+
DELIMITER = /[[:space:]]*/
|
13
14
|
|
14
15
|
def initialize(query)
|
15
16
|
@scanner = StringScanner.new query.strip
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
yielder << Token.new(token)
|
24
|
-
end
|
25
|
-
scanner.skip DELIMITER
|
26
|
-
end
|
19
|
+
def each
|
20
|
+
return to_enum(__callee__) unless block_given?
|
21
|
+
|
22
|
+
until scanner.eos?
|
23
|
+
yield self.next
|
27
24
|
end
|
28
25
|
end
|
29
26
|
|
27
|
+
def next
|
28
|
+
scanner.skip DELIMITER
|
29
|
+
token = scanner.scan COMPOUND_TOKEN
|
30
|
+
raise StopIteration unless token
|
31
|
+
Token.new token
|
32
|
+
end
|
33
|
+
|
30
34
|
def_delegator :scanner, :reset
|
31
|
-
def_delegators :enum, :each, :next
|
32
35
|
|
33
36
|
def simplify
|
34
37
|
scanner.unscan
|
35
|
-
Token.new
|
36
|
-
scanner.skip DELIMITER
|
37
|
-
end
|
38
|
+
Token.new scanner.scan SIMPLE_TOKEN
|
38
39
|
end
|
39
40
|
|
40
41
|
private
|
data/lib/search_lingo/version.rb
CHANGED
data/lib/search_lingo.rb
CHANGED
@@ -1,7 +1,3 @@
|
|
1
1
|
require 'search_lingo/version'
|
2
2
|
require 'search_lingo/abstract_search'
|
3
|
-
|
4
|
-
require 'search_lingo/parsers/date_parser'
|
5
|
-
require 'search_lingo/parsers/date_range_parser'
|
6
|
-
require 'search_lingo/parsers/gte_date_parser'
|
7
|
-
require 'search_lingo/parsers/lte_date_parser'
|
3
|
+
require 'search_lingo/parsers'
|
data/search_lingo.gemspec
CHANGED
@@ -19,6 +19,8 @@ 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.required_ruby_version = '>= 2.1'
|
23
|
+
|
22
24
|
spec.add_development_dependency "bundler", "~> 1.9"
|
23
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
26
|
spec.add_development_dependency 'minitest'
|
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.0.
|
4
|
+
version: 1.0.0.beta3
|
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
|
+
date: 2015-06-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -100,6 +100,8 @@ files:
|
|
100
100
|
- examples/simple.rb
|
101
101
|
- lib/search_lingo.rb
|
102
102
|
- lib/search_lingo/abstract_search.rb
|
103
|
+
- lib/search_lingo/constants.rb
|
104
|
+
- lib/search_lingo/parsers.rb
|
103
105
|
- lib/search_lingo/parsers/date_parser.rb
|
104
106
|
- lib/search_lingo/parsers/date_range_parser.rb
|
105
107
|
- lib/search_lingo/parsers/gte_date_parser.rb
|
@@ -121,7 +123,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
123
|
requirements:
|
122
124
|
- - ">="
|
123
125
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
126
|
+
version: '2.1'
|
125
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
126
128
|
requirements:
|
127
129
|
- - ">"
|