scoped_search 2.0.1

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.
@@ -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