search_lingo 1.0.0.beta2 → 1.0.0.beta3
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 +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
|
- - ">"
|