attr_searchable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,136 @@
1
+
2
+ require "attr_searchable_grammar/attributes"
3
+ require "attr_searchable_grammar/nodes"
4
+
5
+ module AttrSearchableGrammar
6
+ class BaseNode < Treetop::Runtime::SyntaxNode
7
+ attr_writer :model
8
+
9
+ def model
10
+ @model || parent.model
11
+ end
12
+
13
+ def to_arel
14
+ elements.collect(&:to_arel).inject(:and)
15
+ end
16
+
17
+ def elements
18
+ super.select { |element| element.class != Treetop::Runtime::SyntaxNode }
19
+ end
20
+
21
+ def arel_collection_for(key)
22
+ raise AttrSearchable::UnknownColumn, "Unknown key: #{key}" if model.searchable_attributes[key].nil?
23
+
24
+ Attributes::Collection.new model, key
25
+ end
26
+ end
27
+
28
+ class OperatorNode < Treetop::Runtime::SyntaxNode
29
+ def to_arel
30
+ text_value
31
+ end
32
+ end
33
+
34
+ class ComplexExpression < BaseNode; end
35
+ class ParenthesesExpression < BaseNode; end
36
+
37
+ class ComparativeExpression < BaseNode
38
+ def to_arel
39
+ elements[0].arel_collection.send elements[1].to_arel_method, elements[2].text_value
40
+ end
41
+ end
42
+
43
+ class IncludesOperator < OperatorNode
44
+ def to_arel_method
45
+ :matches
46
+ end
47
+ end
48
+
49
+ class EqualOperator < OperatorNode
50
+ def to_arel_method
51
+ :eq
52
+ end
53
+ end
54
+
55
+ class UnequalOperator < OperatorNode
56
+ def to_arel_method
57
+ :not_eq
58
+ end
59
+ end
60
+
61
+ class GreaterEqualOperator < OperatorNode
62
+ def to_arel_method
63
+ :gteq
64
+ end
65
+ end
66
+
67
+ class GreaterOperator < OperatorNode
68
+ def to_arel_method
69
+ :gt
70
+ end
71
+ end
72
+
73
+ class LessEqualOperator < OperatorNode
74
+ def to_arel_method
75
+ :lteq
76
+ end
77
+ end
78
+
79
+ class LessOperator < OperatorNode
80
+ def to_arel_method
81
+ :lt
82
+ end
83
+ end
84
+
85
+ class AnywhereExpression < BaseNode
86
+ def to_arel
87
+ keys = model.searchable_attribute_options.select { |key, value| value[:default] == true }.keys
88
+ keys = model.searchable_attributes.keys if keys.empty?
89
+
90
+ queries = keys.collect { |key| arel_collection_for key }.select { |attribute| attribute.compatible? text_value }.collect { |attribute| attribute.matches text_value }
91
+
92
+ raise AttrSearchable::NoSearchableAttributes unless model.searchable_attributes
93
+
94
+ queries.flatten.inject(:or)
95
+ end
96
+ end
97
+
98
+ class AndExpression < BaseNode
99
+ def to_arel
100
+ [elements.first.to_arel, elements.last.to_arel].inject(:and)
101
+ end
102
+ end
103
+
104
+ class OrExpression < BaseNode
105
+ def to_arel
106
+ [elements.first.to_arel, elements.last.to_arel].inject(:or)
107
+ end
108
+ end
109
+
110
+ class NotExpression < BaseNode
111
+ def to_arel
112
+ elements.first.to_arel.not
113
+ end
114
+ end
115
+
116
+ class Column < BaseNode
117
+ def arel_collection
118
+ arel_collection_for text_value
119
+ end
120
+ end
121
+
122
+ class SingleQuotedValue < BaseNode
123
+ def text_value
124
+ super.gsub /^'|'$/, ""
125
+ end
126
+ end
127
+
128
+ class DoubleQuotedValue < BaseNode
129
+ def text_value
130
+ super.gsub /^"|"$/, ""
131
+ end
132
+ end
133
+
134
+ class Value < BaseNode; end
135
+ end
136
+
@@ -0,0 +1,55 @@
1
+
2
+ grammar AttrSearchableGrammar
3
+ rule complex_expression
4
+ space? (boolean_expression / expression) space? <ComplexExpression>
5
+ end
6
+
7
+ rule boolean_expression
8
+ and_expression
9
+ end
10
+
11
+ rule and_expression
12
+ or_expression (space? ('AND' / 'and') space? / space !('OR' / 'or')) complex_expression <AndExpression> / or_expression
13
+ end
14
+
15
+ rule or_expression
16
+ expression space? ('OR' / 'or') space? (or_expression / expression) <OrExpression> / expression
17
+ end
18
+
19
+ rule expression
20
+ parentheses_expression / not_expression / comparative_expression / anywhere_expression
21
+ end
22
+
23
+ rule parentheses_expression
24
+ '(' complex_expression ')' <ParenthesesExpression>
25
+ end
26
+
27
+ rule not_expression
28
+ ('NOT' space / 'not' space / '-') (comparative_expression / anywhere_expression) <NotExpression>
29
+ end
30
+
31
+ rule comparative_expression
32
+ simple_column space? comparison_operator space? value <ComparativeExpression>
33
+ end
34
+
35
+ rule comparison_operator
36
+ ':' <IncludesOperator> / '=' <EqualOperator> / '!=' <UnequalOperator> / '>=' <GreaterEqualOperator> / '>' <GreaterOperator> / '<=' <LessEqualOperator> / '<' <LessOperator>
37
+ end
38
+
39
+ rule anywhere_expression
40
+ ("'" ([^\']* <AnywhereExpression>) "'") / ('"' ([^\"]* <AnywhereExpression>) '"') / [^\s()]+ <AnywhereExpression>
41
+ end
42
+
43
+ rule simple_column
44
+ [a-zA-Z0-9_.]+ <Column>
45
+ end
46
+
47
+ rule value
48
+ "'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^\s()]+ <Value>
49
+ end
50
+
51
+ rule space
52
+ [\s]+
53
+ end
54
+ end
55
+
data/test/and_test.rb ADDED
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class AndTest < AttrSearchable::TestCase
5
+ def test_and_string
6
+ expected = FactoryGirl.create(:product, :title => "Expected title", :description => "Description")
7
+ rejected = FactoryGirl.create(:product, :title => "Rejected title", :description => "Description")
8
+
9
+ results = Product.search("title: 'Expected title' description: Description")
10
+
11
+ assert_includes results, expected
12
+ refute_includes results, rejected
13
+
14
+ assert_equal results, Product.search("title: 'Expected title' AND description: Description")
15
+ end
16
+
17
+ def test_and_hash
18
+ expected = FactoryGirl.create(:product, :title => "Expected title", :description => "Description")
19
+ rejected = FactoryGirl.create(:product, :title => "Rejected title", :description => "Description")
20
+
21
+ results = Product.search(:and => [{:title => "Expected title"}, {:description => "Description"}])
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
@@ -0,0 +1,44 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class AttrSearchableTest < AttrSearchable::TestCase
5
+ def test_associations
6
+ product = FactoryGirl.create(:product, :comments => [
7
+ FactoryGirl.create(:comment, :title => "Title1", :message => "Message1"),
8
+ FactoryGirl.create(:comment, :title => "Title2", :message => "Message2")
9
+ ])
10
+
11
+ assert_includes Product.search("comment: Title1 comment: Message1"), product
12
+ assert_includes Product.search("comment: Title2 comment: Message2"), product
13
+ end
14
+
15
+ def test_multiple
16
+ product = FactoryGirl.create(:product, :comments => [FactoryGirl.create(:comment, :title => "Title", :message => "Message")])
17
+
18
+ assert_includes Product.search("comment: Title"), product
19
+ assert_includes Product.search("comment: Message"), product
20
+ end
21
+
22
+ def test_default
23
+ product1 = FactoryGirl.create(:product, :title => "Expected")
24
+ product2 = FactoryGirl.create(:product, :description => "Expected")
25
+
26
+ results = Product.search("Expected")
27
+
28
+ assert_includes results, product1
29
+ assert_includes results, product2
30
+ end
31
+
32
+ def test_custom_default
33
+ product1 = FactoryGirl.create(:product, :title => "Expected")
34
+ product2 = FactoryGirl.create(:product, :description => "Expected")
35
+ product3 = FactoryGirl.create(:product, :brand => "Expected")
36
+
37
+ results = with_attr_searchable_options(Product, :primary, :default => true) { Product.search "Expected" }
38
+
39
+ assert_includes results, product1
40
+ assert_includes results, product2
41
+ refute_includes results, product3
42
+ end
43
+ end
44
+
@@ -0,0 +1,47 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class BooleanTest < AttrSearchable::TestCase
5
+ def test_mapping
6
+ product = FactoryGirl.create(:product, :available => true)
7
+
8
+ assert_includes Product.search("available: 1"), product
9
+ assert_includes Product.search("available: true"), product
10
+ assert_includes Product.search("available: yes"), product
11
+
12
+ product = FactoryGirl.create(:product, :available => false)
13
+
14
+ assert_includes Product.search("available: 0"), product
15
+ assert_includes Product.search("available: false"), product
16
+ assert_includes Product.search("available: no"), product
17
+ end
18
+
19
+ def test_anywhere
20
+ product = FactoryGirl.create(:product, :available => true)
21
+
22
+ assert_includes Product.search("true"), product
23
+ refute_includes Product.search("false"), product
24
+ end
25
+
26
+ def test_includes
27
+ product = FactoryGirl.create(:product, :available => true)
28
+
29
+ assert_includes Product.search("available: true"), product
30
+ refute_includes Product.search("available: false"), product
31
+ end
32
+
33
+ def test_equals
34
+ product = FactoryGirl.create(:product, :available => true)
35
+
36
+ assert_includes Product.search("available = true"), product
37
+ refute_includes Product.search("available = false"), product
38
+ end
39
+
40
+ def test_equals_not
41
+ product = FactoryGirl.create(:product, :available => false)
42
+
43
+ assert_includes Product.search("available != true"), product
44
+ refute_includes Product.search("available != false"), product
45
+ end
46
+ end
47
+
data/test/database.yml ADDED
@@ -0,0 +1,17 @@
1
+
2
+ sqlite:
3
+ adapter: sqlite3
4
+ database: ":memory:"
5
+
6
+ mysql:
7
+ adapter: mysql2
8
+ database: attr_searchable
9
+ username: root
10
+ encoding: utf8
11
+
12
+ postgres:
13
+ adapter: postgresql
14
+ database: attr_searchable
15
+ username: postgres
16
+ encoding: utf8
17
+
@@ -0,0 +1,70 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class DatetimeTest < AttrSearchable::TestCase
5
+ def test_mapping
6
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01 12:30:30"))
7
+
8
+ assert_includes Product.search("created_at: 2014"), product
9
+ assert_includes Product.search("created_at: 2014-05"), product
10
+ assert_includes Product.search("created_at: 2014-05-01"), product
11
+ assert_includes Product.search("created_at: '2014-05-01 12:30:30'"), product
12
+ end
13
+
14
+ def test_anywhere
15
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
16
+
17
+ assert_includes Product.search("2014-05-01"), product
18
+ refute_includes Product.search("2014-05-02"), product
19
+ end
20
+
21
+ def test_includes
22
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
23
+
24
+ assert_includes Product.search("created_at: 2014-05-01"), product
25
+ refute_includes Product.search("created_at: 2014-05-02"), product
26
+ end
27
+
28
+ def test_equals
29
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
30
+
31
+ assert_includes Product.search("created_at = 2014-05-01"), product
32
+ refute_includes Product.search("created_at = 2014-05-02"), product
33
+ end
34
+
35
+ def test_equals_not
36
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
37
+
38
+ assert_includes Product.search("created_at != 2014-05-02"), product
39
+ refute_includes Product.search("created_at != 2014-05-01"), product
40
+ end
41
+
42
+ def test_greater
43
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
44
+
45
+ assert_includes Product.search("created_at < 2014-05-02"), product
46
+ refute_includes Product.search("created_at < 2014-05-01"), product
47
+ end
48
+
49
+ def test_greater_equals
50
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
51
+
52
+ assert_includes Product.search("created_at >= 2014-05-01"), product
53
+ refute_includes Product.search("created_at >= 2014-05-02"), product
54
+ end
55
+
56
+ def test_less
57
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-01"))
58
+
59
+ assert_includes Product.search("created_at < 2014-05-02"), product
60
+ refute_includes Product.search("created_at < 2014-05-01"), product
61
+ end
62
+
63
+ def test_less_equals
64
+ product = FactoryGirl.create(:product, :created_at => Time.parse("2014-05-02"))
65
+
66
+ assert_includes Product.search("created_at <= 2014-05-02"), product
67
+ refute_includes Product.search("created_at <= 2014-05-01"), product
68
+ end
69
+ end
70
+
@@ -0,0 +1,61 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class FloatTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :price => 10.5, :created_at => Time.now - 1.day)
7
+
8
+ assert_includes Product.search("10.5"), product
9
+ refute_includes Product.search("11.5"), product
10
+ end
11
+
12
+ def test_includes
13
+ product = FactoryGirl.create(:product, :price => 10.5)
14
+
15
+ assert_includes Product.search("price: 10.5"), product
16
+ refute_includes Product.search("price: 11.5"), product
17
+ end
18
+
19
+ def test_equals
20
+ product = FactoryGirl.create(:product, :price => 10.5)
21
+
22
+ assert_includes Product.search("price = 10.5"), product
23
+ refute_includes Product.search("price = 11.5"), product
24
+ end
25
+
26
+ def test_equals_not
27
+ product = FactoryGirl.create(:product, :price => 10.5)
28
+
29
+ assert_includes Product.search("price != 11.5"), product
30
+ refute_includes Product.search("price != 10.5"), product
31
+ end
32
+
33
+ def test_greater
34
+ product = FactoryGirl.create(:product, :price => 10.5)
35
+
36
+ assert_includes Product.search("price > 10.4"), product
37
+ refute_includes Product.search("price < 10.5"), product
38
+ end
39
+
40
+ def test_greater_equals
41
+ product = FactoryGirl.create(:product, :price => 10.5)
42
+
43
+ assert_includes Product.search("price >= 10.5"), product
44
+ refute_includes Product.search("price >= 10.6"), product
45
+ end
46
+
47
+ def test_less
48
+ product = FactoryGirl.create(:product, :price => 10.5)
49
+
50
+ assert_includes Product.search("price < 10.6"), product
51
+ refute_includes Product.search("price < 10.5"), product
52
+ end
53
+
54
+ def test_less_equals
55
+ product = FactoryGirl.create(:product, :price => 10.5)
56
+
57
+ assert_includes Product.search("price <= 10.5"), product
58
+ refute_includes Product.search("price <= 10.4"), product
59
+ end
60
+ end
61
+
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class FulltextTest < AttrSearchable::TestCase
5
+ def test_complex
6
+ product1 = FactoryGirl.create(:product, :title => "word1")
7
+ product2 = FactoryGirl.create(:product, :title => "word2 word3")
8
+ product3 = FactoryGirl.create(:product, :title => "word2")
9
+
10
+ results = Product.search("title:word1 OR (title:word2 -title:word3)")
11
+
12
+ assert_includes results, product1
13
+ refute_includes results, product2
14
+ assert_includes results, product3
15
+ end
16
+
17
+ def test_mixed
18
+ expected = FactoryGirl.create(:product, :title => "Expected title", :stock => 1)
19
+ rejected = FactoryGirl.create(:product, :title => "Expected title", :stock => 0)
20
+
21
+ results = Product.search("title:Expected title:Title stock > 0")
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
@@ -0,0 +1,61 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class IntegerTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :stock => 1)
7
+
8
+ assert_includes Product.search("1"), product
9
+ refute_includes Product.search("0"), product
10
+ end
11
+
12
+ def test_includes
13
+ product = FactoryGirl.create(:product, :stock => 1)
14
+
15
+ assert_includes Product.search("stock: 1"), product
16
+ refute_includes Product.search("stock: 10"), product
17
+ end
18
+
19
+ def test_equals
20
+ product = FactoryGirl.create(:product, :stock => 1)
21
+
22
+ assert_includes Product.search("stock = 1"), product
23
+ refute_includes Product.search("stock = 0"), product
24
+ end
25
+
26
+ def test_equals_not
27
+ product = FactoryGirl.create(:product, :stock => 1)
28
+
29
+ assert_includes Product.search("stock != 0"), product
30
+ refute_includes Product.search("stock != 1"), product
31
+ end
32
+
33
+ def test_greater
34
+ product = FactoryGirl.create(:product, :stock => 1)
35
+
36
+ assert_includes Product.search("stock > 0"), product
37
+ refute_includes Product.search("stock < 1"), product
38
+ end
39
+
40
+ def test_greater_equals
41
+ product = FactoryGirl.create(:product, :stock => 1)
42
+
43
+ assert_includes Product.search("stock >= 1"), product
44
+ refute_includes Product.search("stock >= 2"), product
45
+ end
46
+
47
+ def test_less
48
+ product = FactoryGirl.create(:product, :stock => 1)
49
+
50
+ assert_includes Product.search("stock < 2"), product
51
+ refute_includes Product.search("stock < 1"), product
52
+ end
53
+
54
+ def test_less_equals
55
+ product = FactoryGirl.create(:product, :stock => 1)
56
+
57
+ assert_includes Product.search("stock <= 1"), product
58
+ refute_includes Product.search("stock <= 0"), product
59
+ end
60
+ end
61
+
data/test/not_test.rb ADDED
@@ -0,0 +1,27 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class NotTest < AttrSearchable::TestCase
5
+ def test_not_string
6
+ expected = FactoryGirl.create(:product, :title => "Expected title")
7
+ rejected = FactoryGirl.create(:product, :title => "Rejected title")
8
+
9
+ results = Product.search("title: Title NOT title: Rejected")
10
+
11
+ assert_includes results, expected
12
+ refute_includes results, rejected
13
+
14
+ assert_equal results, Product.search("title: Title -title: Rejected")
15
+ end
16
+
17
+ def test_not_hash
18
+ expected = FactoryGirl.create(:product, :title => "Expected title")
19
+ rejected = FactoryGirl.create(:product, :title => "Rejected title")
20
+
21
+ results = Product.search(:and => [{:title => "Title"}, {:not => {:title => "Rejected"}}])
22
+
23
+ assert_includes results, expected
24
+ refute_includes results, rejected
25
+ end
26
+ end
27
+
data/test/or_test.rb ADDED
@@ -0,0 +1,29 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class OrTest < AttrSearchable::TestCase
5
+ def test_or_string
6
+ product1 = FactoryGirl.create(:product, :title => "Title1")
7
+ product2 = FactoryGirl.create(:product, :title => "Title2")
8
+ product3 = FactoryGirl.create(:product, :title => "Title3")
9
+
10
+ results = Product.search("title: Title1 OR title: Title2")
11
+
12
+ assert_includes results, product1
13
+ assert_includes results, product2
14
+ refute_includes results, product3
15
+ end
16
+
17
+ def test_or_hash
18
+ product1 = FactoryGirl.create(:product, :title => "Title1")
19
+ product2 = FactoryGirl.create(:product, :title => "Title2")
20
+ product3 = FactoryGirl.create(:product, :title => "Title3")
21
+
22
+ results = Product.search(:or => [{:title => "Title1"}, {:title => "Title2"}])
23
+
24
+ assert_includes results, product1
25
+ assert_includes results, product2
26
+ refute_includes results, product3
27
+ end
28
+ end
29
+
@@ -0,0 +1,84 @@
1
+
2
+ require File.expand_path("../test_helper", __FILE__)
3
+
4
+ class StringTest < AttrSearchable::TestCase
5
+ def test_anywhere
6
+ product = FactoryGirl.create(:product, :title => "Expected title")
7
+
8
+ assert_includes Product.search("Expected"), product
9
+ refute_includes Product.search("Rejected"), product
10
+ end
11
+
12
+ def test_multiple
13
+ product = FactoryGirl.create(:product, :comments => [FactoryGirl.create(:comment, :title => "Expected title", :message => "Expected message")])
14
+
15
+ assert_includes Product.search("Expected"), product
16
+ refute_includes Product.search("Rejected"), product
17
+ end
18
+
19
+ def test_includes
20
+ product = FactoryGirl.create(:product, :title => "Expected")
21
+
22
+ assert_includes Product.search("title: Expected"), product
23
+ refute_includes Product.search("title: Rejected"), product
24
+ end
25
+
26
+ def test_includes_with_left_wildcard
27
+ product = FactoryGirl.create(:product, :title => "Some title")
28
+
29
+ assert_includes Product.search("title: Title"), product
30
+ end
31
+
32
+ def test_includes_without_left_wildcard
33
+ expected = FactoryGirl.create(:product, :brand => "Brand")
34
+ rejected = FactoryGirl.create(:product, :brand => "Rejected brand")
35
+
36
+ results = with_attr_searchable_options(Product, :brand, :left_wildcard => false) { Product.search "brand: Brand" }
37
+
38
+ assert_includes results, expected
39
+ refute_includes results, rejected
40
+ end
41
+
42
+ def test_equals
43
+ product = FactoryGirl.create(:product, :title => "Expected title")
44
+
45
+ assert_includes Product.search("title = 'Expected title'"), product
46
+ refute_includes Product.search("title = Expected"), product
47
+ end
48
+
49
+ def test_equals_not
50
+ product = FactoryGirl.create(:product, :title => "Expected")
51
+
52
+ assert_includes Product.search("title != Rejected"), product
53
+ refute_includes Product.search("title != Expected"), product
54
+ end
55
+
56
+ def test_greater
57
+ product = FactoryGirl.create(:product, :title => "Title B")
58
+
59
+ assert_includes Product.search("title > 'Title A'"), product
60
+ refute_includes Product.search("title > 'Title B'"), product
61
+ end
62
+
63
+ def test_greater_equals
64
+ product = FactoryGirl.create(:product, :title => "Title A")
65
+
66
+ assert_includes Product.search("title >= 'Title A'"), product
67
+ refute_includes Product.search("title >= 'Title B'"), product
68
+ end
69
+
70
+ def test_less
71
+ product = FactoryGirl.create(:product, :title => "Title A")
72
+
73
+ assert_includes Product.search("title < 'Title B'"), product
74
+ refute_includes Product.search("title < 'Title A'"), product
75
+ end
76
+
77
+ def test_less_or_greater
78
+ product = FactoryGirl.create(:product, :title => "Title B")
79
+
80
+ assert_includes Product.search("title <= 'Title B'"), product
81
+ refute_includes Product.search("title <= 'Title A'"), product
82
+ end
83
+ end
84
+