minidusen 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.ruby-version +2 -0
- data/.travis.yml +30 -0
- data/LICENSE +22 -0
- data/README.md +209 -0
- data/Rakefile +42 -0
- data/doc/filtered_index_view.cropped.png +0 -0
- data/gemfiles/Gemfile.3.2.mysql2 +10 -0
- data/gemfiles/Gemfile.3.2.mysql2.lock +59 -0
- data/gemfiles/Gemfile.4.2.mysql2 +9 -0
- data/gemfiles/Gemfile.4.2.mysql2.lock +63 -0
- data/gemfiles/Gemfile.4.2.pg +9 -0
- data/gemfiles/Gemfile.4.2.pg.lock +63 -0
- data/gemfiles/Gemfile.5.0.mysql2 +9 -0
- data/gemfiles/Gemfile.5.0.mysql2.lock +60 -0
- data/gemfiles/Gemfile.5.0.pg +9 -0
- data/gemfiles/Gemfile.5.0.pg.lock +60 -0
- data/lib/minidusen.rb +12 -0
- data/lib/minidusen/active_record_ext.rb +39 -0
- data/lib/minidusen/filter.rb +31 -0
- data/lib/minidusen/parser.rb +35 -0
- data/lib/minidusen/query.rb +54 -0
- data/lib/minidusen/syntax.rb +54 -0
- data/lib/minidusen/token.rb +25 -0
- data/lib/minidusen/util.rb +55 -0
- data/lib/minidusen/version.rb +3 -0
- data/minidusen.gemspec +23 -0
- data/spec/minidusen/active_record_ext_spec.rb +45 -0
- data/spec/minidusen/filter_spec.rb +196 -0
- data/spec/minidusen/parser_spec.rb +22 -0
- data/spec/minidusen/query_spec.rb +18 -0
- data/spec/minidusen/util_spec.rb +3 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/database.rb +50 -0
- data/spec/support/database.sample.yml +10 -0
- data/spec/support/database.travis.yml +9 -0
- data/spec/support/models.rb +81 -0
- metadata +123 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
minidusen (0.7.0)
|
5
|
+
activerecord (>= 3.2)
|
6
|
+
activesupport (>= 3.2)
|
7
|
+
edge_rider (>= 0.2.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.2.5)
|
13
|
+
activesupport (= 4.2.5)
|
14
|
+
builder (~> 3.1)
|
15
|
+
activerecord (4.2.5)
|
16
|
+
activemodel (= 4.2.5)
|
17
|
+
activesupport (= 4.2.5)
|
18
|
+
arel (~> 6.0)
|
19
|
+
activesupport (4.2.5)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
24
|
+
tzinfo (~> 1.1)
|
25
|
+
arel (6.0.3)
|
26
|
+
builder (3.2.2)
|
27
|
+
byebug (8.2.0)
|
28
|
+
diff-lcs (1.2.5)
|
29
|
+
edge_rider (0.3.1)
|
30
|
+
activerecord
|
31
|
+
i18n (0.7.0)
|
32
|
+
json (1.8.3)
|
33
|
+
minitest (5.8.3)
|
34
|
+
pg (0.18.4)
|
35
|
+
rspec (3.4.0)
|
36
|
+
rspec-core (~> 3.4.0)
|
37
|
+
rspec-expectations (~> 3.4.0)
|
38
|
+
rspec-mocks (~> 3.4.0)
|
39
|
+
rspec-core (3.4.1)
|
40
|
+
rspec-support (~> 3.4.0)
|
41
|
+
rspec-expectations (3.4.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.4.0)
|
44
|
+
rspec-mocks (3.4.1)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.4.0)
|
47
|
+
rspec-support (3.4.1)
|
48
|
+
thread_safe (0.3.5)
|
49
|
+
tzinfo (1.2.2)
|
50
|
+
thread_safe (~> 0.1)
|
51
|
+
|
52
|
+
PLATFORMS
|
53
|
+
ruby
|
54
|
+
|
55
|
+
DEPENDENCIES
|
56
|
+
activerecord (~> 4.2.1)
|
57
|
+
byebug
|
58
|
+
minidusen!
|
59
|
+
pg
|
60
|
+
rspec (~> 3.4)
|
61
|
+
|
62
|
+
BUNDLED WITH
|
63
|
+
1.12.5
|
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
minidusen (0.7.0)
|
5
|
+
activerecord (>= 3.2)
|
6
|
+
activesupport (>= 3.2)
|
7
|
+
edge_rider (>= 0.2.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (5.0.0.1)
|
13
|
+
activesupport (= 5.0.0.1)
|
14
|
+
activerecord (5.0.0.1)
|
15
|
+
activemodel (= 5.0.0.1)
|
16
|
+
activesupport (= 5.0.0.1)
|
17
|
+
arel (~> 7.0)
|
18
|
+
activesupport (5.0.0.1)
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (7.1.2)
|
24
|
+
byebug (9.0.5)
|
25
|
+
concurrent-ruby (1.0.2)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
edge_rider (0.3.1)
|
28
|
+
activerecord
|
29
|
+
i18n (0.7.0)
|
30
|
+
minitest (5.9.0)
|
31
|
+
mysql2 (0.4.4)
|
32
|
+
rspec (3.5.0)
|
33
|
+
rspec-core (~> 3.5.0)
|
34
|
+
rspec-expectations (~> 3.5.0)
|
35
|
+
rspec-mocks (~> 3.5.0)
|
36
|
+
rspec-core (3.5.3)
|
37
|
+
rspec-support (~> 3.5.0)
|
38
|
+
rspec-expectations (3.5.0)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.5.0)
|
41
|
+
rspec-mocks (3.5.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.5.0)
|
44
|
+
rspec-support (3.5.0)
|
45
|
+
thread_safe (0.3.5)
|
46
|
+
tzinfo (1.2.2)
|
47
|
+
thread_safe (~> 0.1)
|
48
|
+
|
49
|
+
PLATFORMS
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
activerecord (~> 5.0.0)
|
54
|
+
byebug
|
55
|
+
minidusen!
|
56
|
+
mysql2 (~> 0.4.4)
|
57
|
+
rspec (~> 3.5)
|
58
|
+
|
59
|
+
BUNDLED WITH
|
60
|
+
1.12.5
|
@@ -0,0 +1,60 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
minidusen (0.7.0)
|
5
|
+
activerecord (>= 3.2)
|
6
|
+
activesupport (>= 3.2)
|
7
|
+
edge_rider (>= 0.2.5)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (5.0.0.1)
|
13
|
+
activesupport (= 5.0.0.1)
|
14
|
+
activerecord (5.0.0.1)
|
15
|
+
activemodel (= 5.0.0.1)
|
16
|
+
activesupport (= 5.0.0.1)
|
17
|
+
arel (~> 7.0)
|
18
|
+
activesupport (5.0.0.1)
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
20
|
+
i18n (~> 0.7)
|
21
|
+
minitest (~> 5.1)
|
22
|
+
tzinfo (~> 1.1)
|
23
|
+
arel (7.1.2)
|
24
|
+
byebug (9.0.5)
|
25
|
+
concurrent-ruby (1.0.2)
|
26
|
+
diff-lcs (1.2.5)
|
27
|
+
edge_rider (0.3.1)
|
28
|
+
activerecord
|
29
|
+
i18n (0.7.0)
|
30
|
+
minitest (5.9.0)
|
31
|
+
pg (0.18.4)
|
32
|
+
rspec (3.5.0)
|
33
|
+
rspec-core (~> 3.5.0)
|
34
|
+
rspec-expectations (~> 3.5.0)
|
35
|
+
rspec-mocks (~> 3.5.0)
|
36
|
+
rspec-core (3.5.3)
|
37
|
+
rspec-support (~> 3.5.0)
|
38
|
+
rspec-expectations (3.5.0)
|
39
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
40
|
+
rspec-support (~> 3.5.0)
|
41
|
+
rspec-mocks (3.5.0)
|
42
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
43
|
+
rspec-support (~> 3.5.0)
|
44
|
+
rspec-support (3.5.0)
|
45
|
+
thread_safe (0.3.5)
|
46
|
+
tzinfo (1.2.2)
|
47
|
+
thread_safe (~> 0.1)
|
48
|
+
|
49
|
+
PLATFORMS
|
50
|
+
ruby
|
51
|
+
|
52
|
+
DEPENDENCIES
|
53
|
+
activerecord (~> 5.0.0)
|
54
|
+
byebug
|
55
|
+
minidusen!
|
56
|
+
pg (~> 0.18.4)
|
57
|
+
rspec (~> 3.5)
|
58
|
+
|
59
|
+
BUNDLED WITH
|
60
|
+
1.12.5
|
data/lib/minidusen.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
require 'edge_rider'
|
4
|
+
|
5
|
+
require 'minidusen/version'
|
6
|
+
require 'minidusen/util'
|
7
|
+
require 'minidusen/token'
|
8
|
+
require 'minidusen/parser'
|
9
|
+
require 'minidusen/query'
|
10
|
+
require 'minidusen/syntax'
|
11
|
+
require 'minidusen/filter'
|
12
|
+
require 'minidusen/active_record_ext'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Minidusen
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
def where_like(conditions, options = {})
|
6
|
+
scope = scoped
|
7
|
+
|
8
|
+
ilike_operator = Util.ilike_operator(scope)
|
9
|
+
|
10
|
+
if options[:negate]
|
11
|
+
match_operator = "NOT #{ilike_operator}"
|
12
|
+
join_operator = 'AND'
|
13
|
+
else
|
14
|
+
match_operator = ilike_operator
|
15
|
+
join_operator = 'OR'
|
16
|
+
end
|
17
|
+
|
18
|
+
conditions.each do |field_or_fields, query|
|
19
|
+
fields = Array(field_or_fields).collect do |field|
|
20
|
+
Util.qualify_column_name(scope, field)
|
21
|
+
end
|
22
|
+
Array.wrap(query).each do |phrase|
|
23
|
+
phrase_with_placeholders = fields.collect { |field|
|
24
|
+
"#{field} #{match_operator} ?"
|
25
|
+
}.join(" #{join_operator} ")
|
26
|
+
like_expression = Minidusen::Util.like_expression(phrase)
|
27
|
+
bindings = [like_expression] * fields.size
|
28
|
+
conditions = [ phrase_with_placeholders, *bindings ]
|
29
|
+
scope = scope.where(conditions)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
scope
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
ActiveRecord::Base.send(:extend, Minidusen::ActiveRecordExtensions::ClassMethods)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Minidusen
|
2
|
+
module Filter
|
3
|
+
module ClassMethods
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
attr_accessor :minidusen_syntax
|
8
|
+
|
9
|
+
def filter(field, &block)
|
10
|
+
minidusen_syntax.learn_field(field, &block)
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(base)
|
16
|
+
base.extend(ClassMethods)
|
17
|
+
base.send(:minidusen_syntax=, Syntax.new)
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter(scope, query)
|
21
|
+
minidusen_syntax.search(scope, query)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def minidusen_syntax
|
27
|
+
self.class.send(:minidusen_syntax)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Minidusen
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
TEXT_QUERY = /(?:(\-)?"([^"]+)"|(\-)?([\S]+))/
|
5
|
+
FIELD_QUERY = /(\-)?(\w+)\:#{TEXT_QUERY}/
|
6
|
+
|
7
|
+
def self.parse(query_string)
|
8
|
+
query_string = query_string.dup # we are going to delete substrings in-place
|
9
|
+
query = Query.new
|
10
|
+
extract_field_query_tokens(query_string, query)
|
11
|
+
extract_text_query_tokens(query_string, query)
|
12
|
+
query
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.extract_text_query_tokens(query_string, query)
|
16
|
+
while query_string.sub!(TEXT_QUERY, '')
|
17
|
+
value = "#{$2}#{$4}"
|
18
|
+
exclude = "#{$1}#{$3}" == "-"
|
19
|
+
options = { :field => 'text', :value => value, :exclude => exclude }
|
20
|
+
query << Token.new(options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.extract_field_query_tokens(query_string, query)
|
25
|
+
while query_string.sub!(FIELD_QUERY, '')
|
26
|
+
field = $2
|
27
|
+
value = "#{$4}#{$6}"
|
28
|
+
exclude = "#{$1}" == "-"
|
29
|
+
options = { :field => field, :value => value, :exclude => exclude }
|
30
|
+
query << Token.new(options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Minidusen
|
2
|
+
class Query
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :tokens
|
7
|
+
|
8
|
+
def initialize(initial_tokens = [])
|
9
|
+
@tokens = initial_tokens
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(token)
|
13
|
+
tokens << token
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](index)
|
17
|
+
tokens[index]
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
collect(&:to_s).join(" + ")
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(&block)
|
25
|
+
tokens.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def condensed
|
29
|
+
include_texts = include.select(&:text?).collect(&:value)
|
30
|
+
exclude_texts = exclude.select(&:text?).collect(&:value)
|
31
|
+
field_tokens = tokens.reject(&:text?)
|
32
|
+
|
33
|
+
condensed_tokens = field_tokens
|
34
|
+
if include_texts.present?
|
35
|
+
options = { :field => 'text', :value => include_texts, :exclude => false }
|
36
|
+
condensed_tokens << Token.new(options)
|
37
|
+
end
|
38
|
+
if exclude_texts.present?
|
39
|
+
options = { :field => 'text', :value => exclude_texts, :exclude => true }
|
40
|
+
condensed_tokens << Token.new(options)
|
41
|
+
end
|
42
|
+
self.class.new(condensed_tokens)
|
43
|
+
end
|
44
|
+
|
45
|
+
def include
|
46
|
+
self.class.new tokens.reject(&:exclude?)
|
47
|
+
end
|
48
|
+
|
49
|
+
def exclude
|
50
|
+
self.class.new tokens.select(&:exclude?)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Minidusen
|
2
|
+
class Syntax
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@scopers = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def learn_field(field, &scoper)
|
9
|
+
field = field.to_s
|
10
|
+
@scopers[field] = scoper
|
11
|
+
end
|
12
|
+
|
13
|
+
def search(root_scope, query)
|
14
|
+
query = parse(query) if query.is_a?(String)
|
15
|
+
query = query.condensed
|
16
|
+
matches = apply_query(root_scope, query.include)
|
17
|
+
if query.exclude.any?
|
18
|
+
matches = append_excludes(matches, query.exclude)
|
19
|
+
end
|
20
|
+
matches
|
21
|
+
end
|
22
|
+
|
23
|
+
def fields
|
24
|
+
@scopers
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse(query)
|
28
|
+
Parser.parse(query)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
NONE = lambda do |scope, *args|
|
34
|
+
scope.where('1=2')
|
35
|
+
end
|
36
|
+
|
37
|
+
def apply_query(root_scope, query)
|
38
|
+
scope = root_scope
|
39
|
+
query.each do |token|
|
40
|
+
scoper = @scopers[token.field] || NONE
|
41
|
+
scope = scoper.call(scope, token.value)
|
42
|
+
end
|
43
|
+
scope
|
44
|
+
end
|
45
|
+
|
46
|
+
def append_excludes(matches, exclude_query)
|
47
|
+
excluded_records = apply_query(matches.origin_class, exclude_query)
|
48
|
+
qualified_id_field = Util.qualify_column_name(excluded_records, excluded_records.primary_key)
|
49
|
+
exclude_sql = "#{qualified_id_field} NOT IN (#{excluded_records.select(qualified_id_field).to_sql})"
|
50
|
+
matches.where(exclude_sql)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|