wvanbergen-scoped_search 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.rdoc +48 -32
  2. data/Rakefile +1 -3
  3. data/lib/scoped_search.rb +45 -95
  4. data/lib/scoped_search/adapters.rb +41 -0
  5. data/lib/scoped_search/definition.rb +122 -0
  6. data/lib/scoped_search/query_builder.rb +213 -0
  7. data/lib/scoped_search/query_language.rb +30 -0
  8. data/lib/scoped_search/query_language/ast.rb +141 -0
  9. data/lib/scoped_search/query_language/parser.rb +115 -0
  10. data/lib/scoped_search/query_language/tokenizer.rb +62 -0
  11. data/{test → spec}/database.yml +0 -0
  12. data/spec/integration/api_spec.rb +82 -0
  13. data/spec/integration/ordinal_querying_spec.rb +153 -0
  14. data/spec/integration/relation_querying_spec.rb +258 -0
  15. data/spec/integration/string_querying_spec.rb +187 -0
  16. data/spec/lib/database.rb +44 -0
  17. data/spec/lib/matchers.rb +40 -0
  18. data/spec/lib/mocks.rb +19 -0
  19. data/spec/spec_helper.rb +21 -0
  20. data/spec/unit/ast_spec.rb +197 -0
  21. data/spec/unit/definition_spec.rb +24 -0
  22. data/spec/unit/parser_spec.rb +105 -0
  23. data/spec/unit/query_builder_spec.rb +5 -0
  24. data/spec/unit/tokenizer_spec.rb +97 -0
  25. data/tasks/database_tests.rake +5 -5
  26. data/tasks/github-gem.rake +8 -3
  27. metadata +39 -23
  28. data/lib/scoped_search/query_conditions_builder.rb +0 -209
  29. data/lib/scoped_search/query_language_parser.rb +0 -117
  30. data/lib/scoped_search/reg_tokens.rb +0 -51
  31. data/tasks/documentation.rake +0 -33
  32. data/test/integration/api_test.rb +0 -53
  33. data/test/lib/test_models.rb +0 -148
  34. data/test/lib/test_schema.rb +0 -68
  35. data/test/test_helper.rb +0 -44
  36. data/test/unit/query_conditions_builder_test.rb +0 -410
  37. data/test/unit/query_language_test.rb +0 -155
  38. data/test/unit/search_for_test.rb +0 -124
@@ -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,21 @@
1
+ $:.reject! { |e| e.include? 'TextMate' }
2
+ $: << File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'spec'
6
+ require 'active_record'
7
+
8
+ require 'scoped_search'
9
+ require 'scoped_search/query_language'
10
+
11
+ module ScopedSearch::Spec; end
12
+
13
+ require "#{File.dirname(__FILE__)}/lib/matchers"
14
+ require "#{File.dirname(__FILE__)}/lib/database"
15
+ require "#{File.dirname(__FILE__)}/lib/mocks"
16
+
17
+
18
+ Spec::Runner.configure do |config|
19
+ config.include ScopedSearch::Spec::Matchers
20
+ config.include ScopedSearch::Spec::Mocks
21
+ 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,105 @@
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
+
22
+ it "should create an OR construct" do
23
+ 'some OR simple OR keywords'.should parse_to([:or, 'some', 'simple', 'keywords'])
24
+ end
25
+
26
+ it "should nest an OR as second argument of the AND construct" do
27
+ 'some simple OR keywords'.should parse_to([:and, 'some', [:or, 'simple', 'keywords']])
28
+ end
29
+
30
+ it "should nest an OR as first argument of an AND construct" do
31
+ 'some OR simple keywords'.should parse_to([:and, [:or, 'some', 'simple'], 'keywords'])
32
+ end
33
+
34
+ it "should handle parenthesis as AND block, placed in an OR block" do
35
+ 'some OR (simple keywords)'.should parse_to([:or, 'some', [:and, 'simple', 'keywords']])
36
+ end
37
+
38
+ it "should handle parenthesis as OR block in an AND block" do
39
+ '(some OR simple) keywords'.should parse_to([:and, [:or, 'some', 'simple'], 'keywords'])
40
+ end
41
+
42
+ it "should create a block for the NOT keyword" do
43
+ 'not easy'.should parse_to([:not, 'easy'])
44
+ end
45
+
46
+ it "should create a nsted NOT block" do
47
+ 'not !easy'.should parse_to([:not, [:not, 'easy']])
48
+ end
49
+
50
+ it "should create a block for the NOT keyword in an AND block" do
51
+ 'hard !easy'.should parse_to([:and, 'hard', [:not, 'easy']])
52
+ end
53
+
54
+ it "should create a block for the NOT keyword in an OR block" do
55
+ 'hard || !easy'.should parse_to([:or, 'hard', [:not, 'easy']])
56
+ end
57
+
58
+ it "should create a block for the NOT keyword in an OR block" do
59
+ '!easy OR !hard'.should parse_to([:or, [:not, 'easy'], [:not, 'hard']])
60
+ end
61
+
62
+ it "should nest the NOT blocks correctly according to parantheses" do
63
+ '!(easy OR !hard)'.should parse_to([:not, [:or, 'easy', [:not, 'hard']]])
64
+ end
65
+
66
+ it "should create OR blocks in an AND block" do
67
+ '(a|b)(b|c)'.should parse_to([:and, [:or, 'a', 'b'], [:or, 'b', 'c']])
68
+ end
69
+
70
+ it "should create OR blocks in an explicit AND block" do
71
+ '(a|b)&(b|c)'.should parse_to([:and, [:or, 'a', 'b'], [:or, 'b', 'c']])
72
+ end
73
+
74
+ it "should ignore a comma under normal circumstances" do
75
+ 'a,b'.should parse_to([:and, 'a', 'b'])
76
+ end
77
+
78
+ it "should correctly parse an infix comparison" do
79
+ 'a>b'.should parse_to([:gt, 'a', 'b'])
80
+ end
81
+
82
+ it "should correctly parse a prefix comparison" do
83
+ '<b'.should parse_to([:lt, 'b'])
84
+ end
85
+
86
+ it "should create a comparison in an AND block because of the comma delimiter" do
87
+ 'a, < b'.should parse_to([:and, 'a', [:lt, 'b']])
88
+ end
89
+
90
+ it "should create a infix and prefix comparison in an AND block because of parentheses" do
91
+ '(a = b) >c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
92
+ end
93
+
94
+ it "should create a infix and prefix comparison in an AND block because of a comma" do
95
+ 'a = b, >c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
96
+ end
97
+
98
+ it "should create a infix and prefix comparison in an AND block because of first come first serve" do
99
+ 'a = b > c'.should parse_to([:and, [:eq, 'a', 'b'], [:gt, 'c']])
100
+ end
101
+
102
+ it "should parse a null? keyword" do
103
+ 'set? a b null? c'.should parse_to([:and, [:notnull, 'a'], 'b', [:null, 'c']])
104
+ end
105
+ end
@@ -0,0 +1,5 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch::QueryBuilder do
4
+ it "should be specced"
5
+ end
@@ -0,0 +1,97 @@
1
+ require "#{File.dirname(__FILE__)}/../spec_helper"
2
+
3
+ describe ScopedSearch::QueryLanguage::Tokenizer do
4
+
5
+ it "should create tokens for strings" do
6
+ 'some simple keywords'.should tokenize_to('some', 'simple', 'keywords')
7
+ end
8
+
9
+ it "should ignore excessive whitespace" do
10
+ " with\twhitespace \n".should tokenize_to('with', 'whitespace')
11
+ end
12
+
13
+ it "should leave quoted strings intact" do
14
+ '"quoted string"'.should tokenize_to("quoted string")
15
+ end
16
+
17
+ it "should allow escaping quotes in quoted strings using a backslash" do
18
+ '"quoted \"string"'.should tokenize_to('quoted "string')
19
+ end
20
+
21
+ it "should allow escaping the escape charachter" do
22
+ '"quoted \\\\string"'.should tokenize_to('quoted \\string')
23
+ end
24
+
25
+ it "should handle unclosed quoted string gracefully" do
26
+ '"quoted string'.should tokenize_to("quoted string")
27
+ end
28
+
29
+ it "should tokenize multiple quoted strings" do
30
+ '"quoted string" "another" '.should tokenize_to("quoted string", 'another')
31
+ end
32
+
33
+ it "should tokenize multiple quoted strings without separating whitespace" do
34
+ '"quoted string""another"'.should tokenize_to("quoted string", 'another')
35
+ end
36
+
37
+ it "should tokenize combinations of quoted strings and unquoted strings" do
38
+ '"quoted string" another'.should tokenize_to("quoted string", 'another')
39
+ end
40
+
41
+ it "should tokenize combinations of quoted strings and unquoted strings without whitespace" do
42
+ '"quoted string"another'.should tokenize_to("quoted string", 'another')
43
+ end
44
+
45
+ it "should allow quotes in the middle of a normal string" do
46
+ 'a"b'.should tokenize_to('a"b')
47
+ end
48
+
49
+ it "should parse keyword characters" do
50
+ 'a | -("b""c") & d'.should tokenize_to('a', :or, :not, :lparen, 'b', 'c', :rparen, :and, 'd')
51
+ end
52
+
53
+ it "should tokenize null operators" do
54
+ 'set? a null? b'.should tokenize_to(:notnull, 'a', :null, 'b')
55
+ end
56
+
57
+ it "should parse double keyword characters" do
58
+ 'a || b'.should tokenize_to('a', :or, 'b')
59
+ end
60
+
61
+ it "should parse double keyword characters with whitespace as separate tokens" do
62
+ 'a | | b'.should tokenize_to('a', :or, :or, 'b')
63
+ end
64
+
65
+ it "should parse keyword strings" do
66
+ "a and b".should tokenize_to('a', :and, 'b')
67
+ end
68
+
69
+ it "should parse keyword strings with different capitalizations" do
70
+ "a AnD b".should tokenize_to('a', :and, 'b')
71
+ end
72
+
73
+ it "should not parse keyword strings when quoted" do
74
+ 'a "and" b'.should tokenize_to('a', 'and', 'b')
75
+ end
76
+
77
+ it "should not parse a negation character within a string as NOT keyword" do
78
+ 'a-b'.should tokenize_to('a-b')
79
+ end
80
+
81
+ it "should parse an AND operator within a string as separate token" do
82
+ 'a&b'.should tokenize_to('a', :and, 'b')
83
+ end
84
+
85
+ it "should parse an OR operator within a string as separate token" do
86
+ 'a||b'.should tokenize_to('a', :or, 'b')
87
+ end
88
+
89
+ it "should parse an equals operator within a string as separate token" do
90
+ 'a=b'.should tokenize_to('a', :eq, 'b')
91
+ end
92
+
93
+ it "should not parse an operator within a string as separate token when quoted" do
94
+ '"a=b"'.should tokenize_to('a=b')
95
+ end
96
+
97
+ end