scoped_search 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,49 @@
1
+ ActiveRecord::Migration.verbose = false unless ENV.has_key?('DEBUG')
2
+
3
+ module ScopedSearch::Spec::Database
4
+
5
+ def self.establish_connection
6
+ if ENV['DATABASE']
7
+ self.establish_named_connection(ENV['DATABASE'])
8
+ else
9
+ self.establish_default_connection
10
+ end
11
+ end
12
+
13
+ def self.test_databases
14
+ @database_connections ||= YAML.load(File.read("#{File.dirname(__FILE__)}/../database.yml"))
15
+ @database_connections.keys
16
+ end
17
+
18
+ def self.establish_named_connection(name)
19
+ @database_connections ||= YAML.load(File.read("#{File.dirname(__FILE__)}/../database.yml"))
20
+ raise "#{name} database not configured" if @database_connections[name.to_s].nil?
21
+ ActiveRecord::Base.establish_connection(@database_connections[name.to_s])
22
+ end
23
+
24
+ def self.establish_default_connection
25
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
26
+ end
27
+
28
+ def self.close_connection
29
+ ActiveRecord::Base.remove_connection
30
+ end
31
+
32
+ def self.create_model(fields)
33
+ table_name = "model_#{rand}".gsub(/\./, '')
34
+ ActiveRecord::Migration.create_table(table_name) do |t|
35
+ fields.each do |name, field_type|
36
+ t.send(field_type.to_s.gsub(/^unindexed_/, '').to_sym, name)
37
+ end
38
+ end
39
+
40
+ klass = Class.new(ActiveRecord::Base)
41
+ klass.set_table_name(table_name)
42
+ yield(klass) if block_given?
43
+ return klass
44
+ end
45
+
46
+ def self.drop_model(klass)
47
+ ActiveRecord::Migration.drop_table(klass.table_name)
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ module ScopedSearch::Spec::Matchers
2
+ def be_infix_operator(operator = nil)
3
+ simple_matcher('node to be an infix operator') do |node|
4
+ node.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) &&
5
+ node.infix? && (operator.nil? || operator == node.operator)
6
+ end
7
+ end
8
+
9
+ def be_prefix_operator(operator = nil)
10
+ simple_matcher('node to be an prefix operator') do |node|
11
+ node.kind_of?(ScopedSearch::QueryLanguage::AST::OperatorNode) &&
12
+ node.prefix? && (operator.nil? || operator == node.operator)
13
+ end
14
+ end
15
+
16
+ def be_logical_operator(operator = nil)
17
+ simple_matcher('node to be an logical operator') do |node|
18
+ node.kind_of?(ScopedSearch::QueryLanguage::AST::LogicalOperatorNode) &&
19
+ (operator.nil? || operator == node.operator)
20
+ end
21
+ end
22
+
23
+ def be_leaf_node(value = nil)
24
+ simple_matcher('node to be an logical operator') do |node|
25
+ node.kind_of?(ScopedSearch::QueryLanguage::AST::LeafNode) && (value.nil? || value == node.value)
26
+ end
27
+ end
28
+
29
+ def tokenize_to(*tokens)
30
+ simple_matcher("to tokenize to #{tokens.inspect}") do |string|
31
+ tokens == ScopedSearch::QueryLanguage::Compiler.tokenize(string)
32
+ end
33
+ end
34
+
35
+ def parse_to(tree)
36
+ simple_matcher("to parse to #{tree.inspect}") do |string|
37
+ tree == ScopedSearch::QueryLanguage::Compiler.parse(string).to_a
38
+ end
39
+ end
40
+ end
data/spec/lib/mocks.rb ADDED
@@ -0,0 +1,19 @@
1
+ module ScopedSearch::Spec::Mocks
2
+
3
+ def tree(array)
4
+ ScopedSearch::QueryLanguage::AST.from_array(array)
5
+ end
6
+
7
+ def mock_activerecord_class
8
+ ar_mock = mock('ActiveRecord::Base')
9
+ ar_mock.stub!(:named_scope).with(:search_for, anything)
10
+ ar_mock.stub!(:connection).and_return(mock_database_connection)
11
+ return ar_mock
12
+ end
13
+
14
+ def mock_database_connection
15
+ c_mock = mock('ActiveRecord::Base.connection')
16
+ return c_mock
17
+ end
18
+
19
+ end
@@ -0,0 +1,19 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'rubygems'
4
+ require 'spec'
5
+ require 'active_record'
6
+
7
+ require 'scoped_search'
8
+
9
+ module ScopedSearch::Spec; end
10
+
11
+ require "#{File.dirname(__FILE__)}/lib/matchers"
12
+ require "#{File.dirname(__FILE__)}/lib/database"
13
+ require "#{File.dirname(__FILE__)}/lib/mocks"
14
+
15
+
16
+ Spec::Runner.configure do |config|
17
+ config.include ScopedSearch::Spec::Matchers
18
+ config.include ScopedSearch::Spec::Mocks
19
+ end
@@ -0,0 +1,197 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch::QueryLanguage::AST do
4
+
5
+ describe '.from_array' do
6
+
7
+ context 'parsing a singular value' do
8
+ before(:each) do
9
+ @ast = ScopedSearch::QueryLanguage::AST.from_array('value')
10
+ end
11
+
12
+ it "should create a leaf node for a normal value" do
13
+ @ast.should be_leaf_node('value')
14
+ end
15
+ end
16
+
17
+ context 'parsing a prefix operator' do
18
+ before(:each) do
19
+ @ast = ScopedSearch::QueryLanguage::AST.from_array([:not, 'a'])
20
+ end
21
+
22
+ it "should create a operator node for an array starting with an operator" do
23
+ @ast.should be_prefix_operator(:not)
24
+ end
25
+
26
+ it "should create a child node for every subsequent array item" do
27
+ @ast.should have(1).children
28
+ end
29
+
30
+ it "should create set the RHS the the first child value" do
31
+ @ast.rhs.should be_leaf_node('a')
32
+ end
33
+ end
34
+
35
+ context 'pasring an infix operator' do
36
+ before(:each) do
37
+ @ast = ScopedSearch::QueryLanguage::AST.from_array([:eq, 'a', 'b'])
38
+ end
39
+
40
+ it "should create a operator node for an array starting with an operator" do
41
+ @ast.should be_infix_operator(:eq)
42
+ end
43
+
44
+ it "should create a child node for every subsequent array item" do
45
+ @ast.should have(2).children
46
+ end
47
+
48
+ it "should create set the LHS the the first child value" do
49
+ @ast.lhs.should be_leaf_node('a')
50
+ end
51
+
52
+ it "should create set the RHS the the second child value" do
53
+ @ast.rhs.should be_leaf_node('b')
54
+ end
55
+ end
56
+
57
+ context 'parsing a nested logical construct' do
58
+ before(:each) do
59
+ @ast = ScopedSearch::QueryLanguage::AST.from_array([:and, 'a', [:or, 'b', [:not, 'c']]])
60
+ end
61
+
62
+ it "should create a logical operator node for an array starting with :and" do
63
+ @ast.should be_logical_operator(:and)
64
+ end
65
+
66
+ it "should create a child node for every subsequent array item" do
67
+ @ast.should have(2).children
68
+ end
69
+
70
+ it "should create a nested OR structure for a nested array" do
71
+ @ast.lhs.should be_leaf_node('a')
72
+ end
73
+
74
+ it "should create a nested OR structure for a nested array" do
75
+ @ast.rhs.should be_logical_operator(:or)
76
+ end
77
+
78
+ it "should create a leaf node in the nested OR structure" do
79
+ @ast.rhs.lhs.should be_leaf_node('b')
80
+ end
81
+
82
+ it "should create a NOT operator in the nested OR structure" do
83
+ @ast.rhs.rhs.should be_prefix_operator(:not)
84
+ end
85
+
86
+ it "should create a leaf node in the nested OR structure" do
87
+ @ast.rhs.rhs.rhs.should be_leaf_node('c')
88
+ end
89
+ end
90
+ end
91
+
92
+ # Recursive tree simplification algorithm
93
+ describe '#simplify' do
94
+
95
+ it "should not simplify a leaf node" do
96
+ tree('value').simplify.should eql(tree('value'))
97
+ end
98
+
99
+ it "should not simplify a prefix operator node node" do
100
+ tree([:lt, '1']).simplify.should eql(tree([:lt, '1']))
101
+ end
102
+
103
+ it "should not simplify an infix operator node node" do
104
+ tree([:lt, 'field', '1']).simplify.should eql(tree([:lt, 'field', '1']))
105
+ end
106
+
107
+ it "should simplify a single value in a logical operator" do
108
+ tree([:and, 'a']).simplify.should eql(tree('a'))
109
+ end
110
+
111
+ it "should simplify a single operator in a logical operator" do
112
+ tree([:or, [:gt, '2']]).simplify.should eql(tree([:gt, '2']))
113
+ end
114
+
115
+ it "should not simplify a logical operator with multiple values" do
116
+ tree([:and, 'a', 'b']).simplify.should eql(tree([:and, 'a', 'b']))
117
+ end
118
+
119
+ it "should simplify nested logial operators" do
120
+ tree([:and, [:and, 'a', 'b'], [:and, 'c']]).simplify.should eql(tree([:and, 'a', 'b', 'c']))
121
+ end
122
+
123
+ it "should simplify double nested logial operators" do
124
+ tree([:and, [:and, 'a', [:and, 'b', 'c']]]).simplify.should eql(tree([:and, 'a', 'b', 'c']))
125
+ end
126
+
127
+ it "should not simplify nested operators if the operators are different" do
128
+ tree([:or, 'a', [:and, 'b', 'c']]).simplify.should eql(tree([:or, 'a', [:and, 'b', 'c']]))
129
+ end
130
+ end
131
+ end
132
+
133
+ describe ScopedSearch::QueryLanguage::AST::OperatorNode do
134
+
135
+ context 'with 1 child' do
136
+ before(:each) do
137
+ @node = tree([:not, '1'])
138
+ end
139
+
140
+ it 'should be a prefix operator' do
141
+ @node.should be_prefix
142
+ end
143
+
144
+ it "should not be an infix operator" do
145
+ @node.should_not be_infix
146
+ end
147
+
148
+ it 'should return the first child as RHS' do
149
+ @node.rhs.should eql(@node[0])
150
+ end
151
+
152
+ it "should raise an error of the LHS is requested" do
153
+ lambda { @node.lhs }.should raise_error
154
+ end
155
+ end
156
+
157
+ context 'with 2 children' do
158
+ before(:each) do
159
+ @node = tree([:eq, '1', '2'])
160
+ end
161
+
162
+ it "should be an infix operator" do
163
+ @node.should be_infix
164
+ end
165
+
166
+ it "should not be a prefix operator" do
167
+ @node.should_not be_prefix
168
+ end
169
+
170
+ it "should return the first child as LHS" do
171
+ @node.lhs.should eql(@node[0])
172
+ end
173
+
174
+ it "should return the second child as RHS" do
175
+ @node.rhs.should eql(@node[1])
176
+ end
177
+
178
+ end
179
+
180
+ context 'many children' do
181
+ before(:each) do
182
+ @node = tree([:and, '1', '2', '3', '4'])
183
+ end
184
+
185
+ it "should be an infix operator" do
186
+ @node.should be_infix
187
+ end
188
+
189
+ it "should raise an error of the LHS is requested" do
190
+ lambda { @node.lhs }.should raise_error
191
+ end
192
+
193
+ it "should raise an error of the RHS is requested" do
194
+ lambda { @node.rhs }.should raise_error
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,24 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch::Definition do
4
+
5
+ before(:each) do
6
+ @klass = mock_activerecord_class
7
+ @definition = ScopedSearch::Definition.new(@klass)
8
+ @definition.stub!(:setup_adapter)
9
+ end
10
+
11
+ describe '#initialize' do
12
+
13
+ it "should create the named scope when" do
14
+ @klass.should_receive(:named_scope).with(:search_for, instance_of(Proc))
15
+ ScopedSearch::Definition.new(@klass)
16
+ end
17
+
18
+ it "should not create the named scope if it already exists" do
19
+ @klass.stub!(:search_for)
20
+ @klass.should_not_receive(:named_scope)
21
+ ScopedSearch::Definition.new(@klass)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,104 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch::QueryLanguage::Parser do
4
+
5
+ it "should create a 1-node tree for a single keyword" do
6
+ 'single'.should parse_to('single')
7
+ end
8
+
9
+ it "should create a two-item AND construct for two keywords" do
10
+ 'double_1 double_2'.should parse_to([:and, 'double_1', 'double_2'])
11
+ end
12
+
13
+ it "should create a three-item AND construct for three keywords" do
14
+ 'triplet_1 triplet_2 triplet_3'.should parse_to([:and, 'triplet_1', 'triplet_2', 'triplet_3'])
15
+ end
16
+
17
+ it "should create a four-item AND construct by simplifying AND constructs" do
18
+ '1 (2 (3 4))'.should parse_to([:and, '1', '2', '3', '4'])
19
+ end
20
+
21
+ it "should create an OR construct" do
22
+ 'some OR simple OR keywords'.should parse_to([:or, 'some', 'simple', 'keywords'])
23
+ end
24
+
25
+ it "should nest an OR as second argument of the AND construct" do
26
+ 'some simple OR keywords'.should parse_to([:and, 'some', [:or, 'simple', 'keywords']])
27
+ end
28
+
29
+ it "should nest an OR as first argument of an AND construct" do
30
+ 'some OR simple keywords'.should parse_to([:and, [:or, 'some', 'simple'], 'keywords'])
31
+ end
32
+
33
+ it "should handle parenthesis as AND block, placed in an OR block" do
34
+ 'some OR (simple keywords)'.should parse_to([:or, 'some', [:and, 'simple', 'keywords']])
35
+ end
36
+
37
+ it "should handle parenthesis as OR block in an AND block" do
38
+ '(some OR simple) keywords'.should parse_to([:and, [:or, 'some', 'simple'], 'keywords'])
39
+ end
40
+
41
+ it "should create a block for the NOT keyword" do
42
+ 'not easy'.should parse_to([:not, 'easy'])
43
+ end
44
+
45
+ it "should create a nsted NOT block" do
46
+ 'not !easy'.should parse_to([:not, [:not, 'easy']])
47
+ end
48
+
49
+ it "should create a block for the NOT keyword in an AND block" do
50
+ 'hard !easy'.should parse_to([:and, 'hard', [:not, 'easy']])
51
+ end
52
+
53
+ it "should create a block for the NOT keyword in an OR block" do
54
+ 'hard || !easy'.should parse_to([:or, 'hard', [:not, 'easy']])
55
+ end
56
+
57
+ it "should create a block for the NOT keyword in an OR block" do
58
+ '!easy OR !hard'.should parse_to([:or, [:not, 'easy'], [:not, 'hard']])
59
+ end
60
+
61
+ it "should nest the NOT blocks correctly according to parantheses" do
62
+ '!(easy OR !hard)'.should parse_to([:not, [:or, 'easy', [:not, 'hard']]])
63
+ end
64
+
65
+ it "should create OR blocks in an AND block" do
66
+ '(a|b)(b|c)'.should parse_to([:and, [:or, 'a', 'b'], [:or, 'b', 'c']])
67
+ end
68
+
69
+ it "should create OR blocks in an explicit AND block" do
70
+ '(a|b)&(b|c)'.should parse_to([:and, [:or, 'a', 'b'], [:or, 'b', 'c']])
71
+ end
72
+
73
+ it "should ignore a comma under normal circumstances" do
74
+ 'a,b'.should parse_to([:and, 'a', 'b'])
75
+ end
76
+
77
+ it "should correctly parse an infix comparison" do
78
+ 'a>b'.should parse_to([:gt, 'a', 'b'])
79
+ end
80
+
81
+ it "should correctly parse a prefix comparison" do
82
+ '<b'.should parse_to([:lt, 'b'])
83
+ end
84
+
85
+ it "should create a comparison in an AND block because of the comma delimiter" do
86
+ 'a, < b'.should parse_to([:and, 'a', [:lt, 'b']])
87
+ end
88
+
89
+ it "should create a infix and prefix comparison in an AND block because of parentheses" do
90
+ '(a = b) >c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
91
+ end
92
+
93
+ it "should create a infix and prefix comparison in an AND block because of a comma" do
94
+ 'a = b, >c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
95
+ end
96
+
97
+ it "should create a infix and prefix comparison in an AND block because of first come first serve" do
98
+ 'a = b > c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
99
+ end
100
+
101
+ it "should parse a null? keyword" do
102
+ 'set? a b null? c'.should parse_to([:and, [:notnull, 'a'], 'b', [:null, 'c']])
103
+ end
104
+ end