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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3dfc725892e1b65db47327257cd8c082536dd74b
4
- data.tar.gz: 78e4297906559efcb5cedc54b003cf2abfaf4ac1
3
+ metadata.gz: 5f1e9e130f36eb2a7481db8481a510606e0f250d
4
+ data.tar.gz: 9add0798376d6e52412102eb0ba76c542f43ca88
5
5
  SHA512:
6
- metadata.gz: 5359eff62098245796f55f5abe4d3e663e0e56677b7925ba1267bf0e00dda71cfefa84d87c416a57a6b7f21699ca7b2b9914d96b74f37b02fa0597a35c32b931
7
- data.tar.gz: aaa8f3414ebf996194ce415e0270fa2321290900ac98092ea34d2dcb43608e3f6df4e62146fea960814063f753ca7b05fd9fb8a3637399dd1459fe786c7700aa
6
+ metadata.gz: d299c1e7b9fac6aeb2d17ccaaeb4ecacf3afcb42e71b009c9b2da5b9f6fd1b42238e4d33c3338ed1efddea1d776f92df352ad620e68dcbafa96f83fc630305ef
7
+ data.tar.gz: 8851f6d29a00c39bdce63e8381603a6b2970e8b0804b9549d22d2510203caaef71b04530d04deab7b799da9d240ebb4235429cdedda0e1beca53346eb3ea6bb5
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  /.bundle/
2
+ /.bundle-*/
2
3
  /.yardoc
3
4
  /Gemfile.lock
4
5
  /_yardoc/
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 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.)
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 (typically nil).
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, tokenizer: Tokenizer)
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, :tokenizer
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, '.parse must be called with callable or block'
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
@@ -0,0 +1,4 @@
1
+ module SearchLingo
2
+ OPERATOR = /[[:alnum:]]+/
3
+ TERM = /"[^"]+"|[[:graph:]]+/
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'search_lingo/parsers/date_parser'
2
+ require 'search_lingo/parsers/date_range_parser'
3
+ require 'search_lingo/parsers/gte_date_parser'
4
+ require 'search_lingo/parsers/lte_date_parser'
@@ -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
- FORMAT = %r{\A(?:(\S+):\s*)?"?(.+?)"?\z}
6
+ STRUCTURE = /\A(?:(#{OPERATOR}):[[:space:]]*)?"?(.+?)"?\z/
6
7
 
7
8
  def operator
8
- self[FORMAT, 1]
9
+ self[STRUCTURE, 1]
9
10
  end
10
11
 
11
12
  def term
12
- self[FORMAT, 2]
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>' % [self.class, super, operator.inspect, term.inspect]
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
- SIMPLE = %r{"[^"]*"|[[:graph:]]+}
11
- COMPOUND = %r{(?:[[:graph:]]+:[[:space:]]*)?#{SIMPLE}}
12
- DELIMITER = %r{[[:space:]]*}
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 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
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(scanner.scan(SIMPLE)).tap do
36
- scanner.skip DELIMITER
37
- end
38
+ Token.new scanner.scan SIMPLE_TOKEN
38
39
  end
39
40
 
40
41
  private
@@ -1,3 +1,3 @@
1
1
  module SearchLingo
2
- VERSION = '1.0.0.beta2'
2
+ VERSION = '1.0.0.beta3'
3
3
  end
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.beta2
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-04 00:00:00.000000000 Z
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: '0'
126
+ version: '2.1'
125
127
  required_rubygems_version: !ruby/object:Gem::Requirement
126
128
  requirements:
127
129
  - - ">"