scoped_search 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +107 -0
- data/Rakefile +4 -0
- data/init.rb +1 -0
- data/lib/scoped_search.rb +91 -0
- data/lib/scoped_search/definition.rb +145 -0
- data/lib/scoped_search/query_builder.rb +296 -0
- data/lib/scoped_search/query_language.rb +38 -0
- data/lib/scoped_search/query_language/ast.rb +141 -0
- data/lib/scoped_search/query_language/parser.rb +120 -0
- data/lib/scoped_search/query_language/tokenizer.rb +78 -0
- data/scoped_search.gemspec +32 -0
- data/spec/database.yml +25 -0
- data/spec/integration/api_spec.rb +82 -0
- data/spec/integration/ordinal_querying_spec.rb +158 -0
- data/spec/integration/relation_querying_spec.rb +262 -0
- data/spec/integration/string_querying_spec.rb +192 -0
- data/spec/lib/database.rb +49 -0
- data/spec/lib/matchers.rb +40 -0
- data/spec/lib/mocks.rb +19 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/unit/ast_spec.rb +197 -0
- data/spec/unit/definition_spec.rb +24 -0
- data/spec/unit/parser_spec.rb +104 -0
- data/spec/unit/query_builder_spec.rb +22 -0
- data/spec/unit/tokenizer_spec.rb +97 -0
- data/tasks/github-gem.rake +323 -0
- metadata +117 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage
|
2
|
+
|
3
|
+
require 'scoped_search/query_language/ast'
|
4
|
+
require 'scoped_search/query_language/tokenizer'
|
5
|
+
require 'scoped_search/query_language/parser'
|
6
|
+
|
7
|
+
# The Compiler class can compile a query string into an Abstract Syntax Tree,
|
8
|
+
# which in turn is used to build the SQL query.
|
9
|
+
#
|
10
|
+
# This class inclused the Tokenizer module to transform the query stream into
|
11
|
+
# a stream of tokens, and includes the Parser module that will transform the
|
12
|
+
# stream of tokens into an Abstract Syntax Tree (AST).
|
13
|
+
class Compiler
|
14
|
+
|
15
|
+
include Tokenizer
|
16
|
+
include Parser
|
17
|
+
include Enumerable
|
18
|
+
|
19
|
+
def initialize(str) # :nodoc:
|
20
|
+
@str = str
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parser a query string to return an abstract syntax tree.
|
24
|
+
def self.parse(str)
|
25
|
+
compiler = self.new(str)
|
26
|
+
compiler.parse
|
27
|
+
end
|
28
|
+
|
29
|
+
# Tokenizes a query string to return a stream of tokens.
|
30
|
+
def self.tokenize(str)
|
31
|
+
compiler = self.new(str)
|
32
|
+
compiler.tokenize
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module ScopedSearch::QueryLanguage::AST
|
2
|
+
|
3
|
+
# Constructs an AST from an array notation.
|
4
|
+
def self.from_array(arg)
|
5
|
+
if arg.kind_of?(Array)
|
6
|
+
operator = arg.shift
|
7
|
+
case operator
|
8
|
+
when :and, :or
|
9
|
+
LogicalOperatorNode.new(operator, arg.map { |c| from_array(c) })
|
10
|
+
when Symbol
|
11
|
+
OperatorNode.new(operator, arg.map { |c| from_array(c) })
|
12
|
+
else
|
13
|
+
raise ScopedSearch::Exception, "Not a valid array representation of an AST!"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
return LeafNode.new(arg)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Base AST node class. Instances of this class are used to represent an abstract syntax tree.
|
21
|
+
# This syntax tree is created by the ScopedSearch::QueryLanguage parser and visited by the
|
22
|
+
# ScopedSearch::QueryBuilder to create SQL query conditions.
|
23
|
+
class Node
|
24
|
+
|
25
|
+
def inspect # :nodoc
|
26
|
+
"<AST::#{self.class.to_s.split('::').last} #{self.to_a.inspect}>"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Tree simplification. By default, do nothing and return the node as is.
|
30
|
+
def simplify
|
31
|
+
return self
|
32
|
+
end
|
33
|
+
|
34
|
+
def compatible_with(node) # :nodoc
|
35
|
+
false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# AST lead node. This node represents leafs in the AST and can represent
|
40
|
+
# either a search phrase or a search field name.
|
41
|
+
class LeafNode < Node
|
42
|
+
attr_reader :value
|
43
|
+
|
44
|
+
def initialize(value) # :nodoc
|
45
|
+
@value = value
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return an array representation for the node
|
49
|
+
def to_a
|
50
|
+
value
|
51
|
+
end
|
52
|
+
|
53
|
+
def eql?(node) # :nodoc
|
54
|
+
node.kind_of?(LeafNode) && node.value == value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# AST class for representing operators in the query. An operator node has an operator
|
59
|
+
# and operands that are represented as AST child nodes. Usually, operator nodes have
|
60
|
+
# one or two children.
|
61
|
+
# For logical operators, a distinct subclass exists to implement some tree
|
62
|
+
# simplification rules.
|
63
|
+
class OperatorNode < Node
|
64
|
+
attr_reader :operator
|
65
|
+
attr_reader :children
|
66
|
+
|
67
|
+
def initialize(operator, children) # :nodoc
|
68
|
+
@operator = operator
|
69
|
+
@children = children
|
70
|
+
end
|
71
|
+
|
72
|
+
# Tree simplicication: returns itself after simpifying its children
|
73
|
+
def simplify
|
74
|
+
@children = children.map { |c| c.simplify }
|
75
|
+
return self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return an array representation for the node
|
79
|
+
def to_a
|
80
|
+
[@operator] + @children.map { |c| c.to_a }
|
81
|
+
end
|
82
|
+
|
83
|
+
def eql?(node) # :nodoc
|
84
|
+
node.kind_of?(OperatorNode) && node.operator == operator && node.children.eql?(children)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Return the left-hand side (LHS) operand for this operator.
|
88
|
+
def lhs
|
89
|
+
raise ScopedSearch::Exception, "Operator does not have a LHS" if prefix?
|
90
|
+
raise ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS" if children.length > 2
|
91
|
+
children[0]
|
92
|
+
end
|
93
|
+
|
94
|
+
# Return the right-hand side (RHS) operand for this operator.
|
95
|
+
def rhs
|
96
|
+
raise ScopedSearch::Exception, "Operators with more than 2 children do not have LHS/RHS" if children.length > 2
|
97
|
+
children.length == 1 ? children[0] : children[1]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if this is an infix operator
|
101
|
+
def infix?
|
102
|
+
children.length > 1
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns true if this is a prefix operator
|
106
|
+
def prefix?
|
107
|
+
children.length == 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns a child node by index, starting with 0.
|
111
|
+
def [](child_nr)
|
112
|
+
children[child_nr]
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
# AST class for representing AND or OR constructs.
|
118
|
+
# Logical constructs can be simplified resulting in a less complex AST.
|
119
|
+
class LogicalOperatorNode < OperatorNode
|
120
|
+
|
121
|
+
# Checks whether another node is comparable so that it can be used for tree simplification.
|
122
|
+
# A node can only be simplified if the logical operator is equal.
|
123
|
+
def compatible_with(node)
|
124
|
+
node.kind_of?(LogicalOperatorNode) && node.operator == self.operator
|
125
|
+
end
|
126
|
+
|
127
|
+
# Simplifies nested AND and OR constructs to single constructs with multiple arguments:
|
128
|
+
# e.g. (a AND (b AND c)) -> (a AND b AND c)
|
129
|
+
def simplify
|
130
|
+
if children.length == 1
|
131
|
+
# AND or OR constructs do nothing if they only have one operand
|
132
|
+
# So remove the logal operator from the AST by simply using the opeand
|
133
|
+
return children.first.simplify
|
134
|
+
else
|
135
|
+
# nested AND or OR constructs can be combined into one construct
|
136
|
+
@children = children.map { |c| c.simplify }.map { |c| self.compatible_with(c) ? c.children : c }.flatten
|
137
|
+
return self
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# The Parser module adss methods to the query language compiler that transform a string
|
2
|
+
# into an abstract syntax tree, which can be used for query generation.
|
3
|
+
#
|
4
|
+
# This module depends on the tokeinzer module to transform the string into a stream
|
5
|
+
# of tokens, which is more appropriate for parsing. The parser itself is a LL(1)
|
6
|
+
# recursive descent parser.
|
7
|
+
module ScopedSearch::QueryLanguage::Parser
|
8
|
+
|
9
|
+
DEFAULT_SEQUENCE_OPERATOR = :and
|
10
|
+
|
11
|
+
LOGICAL_INFIX_OPERATORS = [:and, :or]
|
12
|
+
LOGICAL_PREFIX_OPERATORS = [:not]
|
13
|
+
NULL_PREFIX_OPERATORS = [:null, :notnull]
|
14
|
+
COMPARISON_OPERATORS = [:eq, :ne, :gt, :gte, :lt, :lte, :like, :unlike]
|
15
|
+
ALL_INFIX_OPERATORS = LOGICAL_INFIX_OPERATORS + COMPARISON_OPERATORS
|
16
|
+
ALL_PREFIX_OPERATORS = LOGICAL_PREFIX_OPERATORS + COMPARISON_OPERATORS + NULL_PREFIX_OPERATORS
|
17
|
+
|
18
|
+
# Start the parsing process by parsing an expression sequence
|
19
|
+
def parse
|
20
|
+
@tokens = tokenize
|
21
|
+
parse_expression_sequence(true).simplify
|
22
|
+
end
|
23
|
+
|
24
|
+
# Parses a sequence of expressions
|
25
|
+
def parse_expression_sequence(initial = false)
|
26
|
+
expressions = []
|
27
|
+
next_token if !initial && peek_token == :lparen # skip staring :lparen
|
28
|
+
expressions << parse_logical_expression until peek_token.nil? || peek_token == :rparen
|
29
|
+
next_token if !initial && peek_token == :rparen # skip final :rparen
|
30
|
+
return ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(DEFAULT_SEQUENCE_OPERATOR, expressions)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parses a logical expression.
|
34
|
+
def parse_logical_expression
|
35
|
+
lhs = case peek_token
|
36
|
+
when nil; nil
|
37
|
+
when :lparen; parse_expression_sequence
|
38
|
+
when :not; parse_logical_not_expression
|
39
|
+
when :null, :notnull; parse_null_expression
|
40
|
+
else; parse_comparison
|
41
|
+
end
|
42
|
+
|
43
|
+
if LOGICAL_INFIX_OPERATORS.include?(peek_token)
|
44
|
+
operator = next_token
|
45
|
+
rhs = parse_logical_expression
|
46
|
+
ScopedSearch::QueryLanguage::AST::LogicalOperatorNode.new(operator, [lhs, rhs])
|
47
|
+
else
|
48
|
+
lhs
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Parses a NOT expression
|
53
|
+
def parse_logical_not_expression
|
54
|
+
next_token # = skip NOT operator
|
55
|
+
negated_expression = case peek_token
|
56
|
+
when :not; parse_logical_not_expression
|
57
|
+
when :lparen; parse_expression_sequence
|
58
|
+
else parse_comparison
|
59
|
+
end
|
60
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(:not, [negated_expression])
|
61
|
+
end
|
62
|
+
|
63
|
+
# Parses a set? or null? expression
|
64
|
+
def parse_null_expression
|
65
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(next_token, [parse_value])
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parses a comparison
|
69
|
+
def parse_comparison
|
70
|
+
next_token if peek_token == :comma # skip comma
|
71
|
+
return (String === peek_token) ? parse_infix_comparison : parse_prefix_comparison
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parses a prefix comparison, i.e. without an explicit field: <operator> <value>
|
75
|
+
def parse_prefix_comparison
|
76
|
+
return ScopedSearch::QueryLanguage::AST::OperatorNode.new(next_token, [parse_value])
|
77
|
+
end
|
78
|
+
|
79
|
+
# Parses an infix expression, i.e. <field> <operator> <value>
|
80
|
+
def parse_infix_comparison
|
81
|
+
lhs = parse_value
|
82
|
+
return case peek_token
|
83
|
+
when nil
|
84
|
+
lhs
|
85
|
+
when :comma
|
86
|
+
next_token # skip comma
|
87
|
+
lhs
|
88
|
+
else
|
89
|
+
if COMPARISON_OPERATORS.include?(peek_token)
|
90
|
+
comparison_operator = next_token
|
91
|
+
rhs = parse_value
|
92
|
+
ScopedSearch::QueryLanguage::AST::OperatorNode.new(comparison_operator, [lhs, rhs])
|
93
|
+
else
|
94
|
+
lhs
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Parses a single value.
|
100
|
+
# This can either be a constant value or a field name.
|
101
|
+
def parse_value
|
102
|
+
raise ScopedSearch::QueryNotSupported, "Value expected but found #{peek_token.inspect}" unless String === peek_token
|
103
|
+
ScopedSearch::QueryLanguage::AST::LeafNode.new(next_token)
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
def current_token # :nodoc:
|
109
|
+
@current_token
|
110
|
+
end
|
111
|
+
|
112
|
+
def peek_token(amount = 1) # :nodoc:
|
113
|
+
@tokens[amount - 1]
|
114
|
+
end
|
115
|
+
|
116
|
+
def next_token # :nodoc:
|
117
|
+
@current_token = @tokens.shift
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# The Tokenizer module adds methods to the query language compiler that transforms a query string
|
2
|
+
# into a stream of tokens, which are more appropriate for parsing a query string.
|
3
|
+
module ScopedSearch::QueryLanguage::Tokenizer
|
4
|
+
|
5
|
+
# All keywords that the language supports
|
6
|
+
KEYWORDS = { 'and' => :and, 'or' => :or, 'not' => :not, 'set?' => :notnull, 'null?' => :null }
|
7
|
+
|
8
|
+
# Every operator the language supports.
|
9
|
+
OPERATORS = { '&' => :and, '|' => :or, '&&' => :and, '||' => :or, '-'=> :not, '!' => :not, '~' => :like, '!~' => :unlike,
|
10
|
+
'=' => :eq, '==' => :eq, '!=' => :ne, '<>' => :ne, '>' => :gt, '<' => :lt, '>=' => :gte, '<=' => :lte }
|
11
|
+
|
12
|
+
# Tokenizes the string and returns the result as an array of tokens.
|
13
|
+
def tokenize
|
14
|
+
@current_char_pos = -1
|
15
|
+
to_a
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns the current character of the string
|
19
|
+
def current_char
|
20
|
+
@current_char
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns a following character of the string (by default, the next
|
24
|
+
# character), without updating the position pointer.
|
25
|
+
def peek_char(amount = 1)
|
26
|
+
@str[@current_char_pos + amount, 1]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the next character of the string, and moves the position
|
30
|
+
# pointer one step forward
|
31
|
+
def next_char
|
32
|
+
@current_char_pos += 1
|
33
|
+
@current_char = @str[@current_char_pos, 1]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Tokenizes the string by iterating over the characters.
|
37
|
+
def each_token(&block)
|
38
|
+
while next_char
|
39
|
+
case current_char
|
40
|
+
when /^\s?$/; # ignore
|
41
|
+
when '('; yield(:lparen)
|
42
|
+
when ')'; yield(:rparen)
|
43
|
+
when ','; yield(:comma)
|
44
|
+
when /\&|\||=|<|>|!|~|-/; tokenize_operator(&block)
|
45
|
+
when '"'; tokenize_quoted_keyword(&block)
|
46
|
+
else; tokenize_keyword(&block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Tokenizes an operator that occurs in the OPERATORS hash
|
52
|
+
def tokenize_operator(&block)
|
53
|
+
operator = current_char
|
54
|
+
operator << next_char if OPERATORS.has_key?(operator + peek_char)
|
55
|
+
yield(OPERATORS[operator])
|
56
|
+
end
|
57
|
+
|
58
|
+
# Tokenizes a keyword, and converts it to a Symbol if it is recognized as a
|
59
|
+
# reserved language keyword (the KEYWORDS array).
|
60
|
+
def tokenize_keyword(&block)
|
61
|
+
keyword = current_char
|
62
|
+
keyword << next_char while /[^=<>\s\&\|\)\(,]/ =~ peek_char
|
63
|
+
KEYWORDS.has_key?(keyword.downcase) ? yield(KEYWORDS[keyword.downcase]) : yield(keyword)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Tokenizes a keyword that is quoted using double quotes. Allows escaping
|
67
|
+
# of double quote characters by backslashes.
|
68
|
+
def tokenize_quoted_keyword(&block)
|
69
|
+
keyword = ""
|
70
|
+
until next_char.nil? || current_char == '"'
|
71
|
+
keyword << (current_char == "\\" ? next_char : current_char)
|
72
|
+
end
|
73
|
+
yield(keyword)
|
74
|
+
end
|
75
|
+
|
76
|
+
alias :each :each_token
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'scoped_search'
|
3
|
+
|
4
|
+
# Do not change the version and date fields by hand. This will be done
|
5
|
+
# automatically by the gem release script.
|
6
|
+
s.version = "2.0.1"
|
7
|
+
s.date = "2009-10-02"
|
8
|
+
|
9
|
+
s.summary = "A Rails plugin to search your models with a simple query language, implemented using a named_scope"
|
10
|
+
s.description = <<EOS
|
11
|
+
Scoped search makes it easy to search your ActiveRecord-based models.
|
12
|
+
It will create a named scope :search_for that can be called with a query string. It will build an SQL query using
|
13
|
+
the provided query string and a definition that specifies on what fields to search. Because the functionality is
|
14
|
+
built on named_scope, the result of the search_for call can be used like any other named_scope, so it can be
|
15
|
+
chained with another scope or combined with will_paginate."
|
16
|
+
EOS
|
17
|
+
|
18
|
+
s.authors = ['Willem van Bergen', 'Wes Hays']
|
19
|
+
s.email = ['willem@vanbergen.org', 'weshays@gbdev.com']
|
20
|
+
s.homepage = 'http://wiki.github.com/wvanbergen/scoped_search'
|
21
|
+
|
22
|
+
s.add_runtime_dependency('activerecord', '>= 2.1.0')
|
23
|
+
s.add_development_dependency('rspec', '>= 1.1.4')
|
24
|
+
|
25
|
+
s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
|
26
|
+
s.extra_rdoc_files = ['README.rdoc']
|
27
|
+
|
28
|
+
# Do not change the files and test_files fields by hand. This will be done
|
29
|
+
# automatically by the gem release script.
|
30
|
+
s.files = %w(spec/spec_helper.rb spec/integration/string_querying_spec.rb spec/integration/relation_querying_spec.rb .gitignore spec/lib/mocks.rb scoped_search.gemspec lib/scoped_search/query_language/parser.rb LICENSE spec/lib/matchers.rb lib/scoped_search/definition.rb init.rb spec/unit/tokenizer_spec.rb spec/unit/parser_spec.rb spec/unit/ast_spec.rb lib/scoped_search/query_language/ast.rb spec/lib/database.rb Rakefile tasks/github-gem.rake spec/unit/query_builder_spec.rb lib/scoped_search/query_language.rb lib/scoped_search/query_builder.rb README.rdoc spec/unit/definition_spec.rb spec/database.yml spec/integration/api_spec.rb spec/integration/ordinal_querying_spec.rb lib/scoped_search/query_language/tokenizer.rb lib/scoped_search.rb)
|
31
|
+
s.test_files = %w(spec/integration/string_querying_spec.rb spec/integration/relation_querying_spec.rb spec/unit/tokenizer_spec.rb spec/unit/parser_spec.rb spec/unit/ast_spec.rb spec/unit/query_builder_spec.rb spec/unit/definition_spec.rb spec/integration/api_spec.rb spec/integration/ordinal_querying_spec.rb)
|
32
|
+
end
|
data/spec/database.yml
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# rake spec will try to run the integration specs on all the following
|
2
|
+
# database connections to test the different adapters.
|
3
|
+
#
|
4
|
+
# The syntax of this file is similar to the database.yml file in a
|
5
|
+
# Rails application. You can change these parameters to your setup,
|
6
|
+
# or comment them out if you have no such database connections available
|
7
|
+
# to you.
|
8
|
+
|
9
|
+
sqlite3:
|
10
|
+
adapter: "sqlite3"
|
11
|
+
database: ":memory:"
|
12
|
+
|
13
|
+
mysql:
|
14
|
+
adapter: "mysql"
|
15
|
+
host: "localhost"
|
16
|
+
user: "root"
|
17
|
+
password:
|
18
|
+
database: "scoped_search_test"
|
19
|
+
|
20
|
+
postgresql:
|
21
|
+
adapter: "postgresql"
|
22
|
+
host: "localhost"
|
23
|
+
username: "scoped_search"
|
24
|
+
password: "scoped_search"
|
25
|
+
database: "scoped_search_test"
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/../spec_helper"
|
2
|
+
|
3
|
+
describe ScopedSearch, "API" do
|
4
|
+
|
5
|
+
# This spec requires the API to be stable, so that projects using
|
6
|
+
# scoped_search do not have to update their code if a new (minor)
|
7
|
+
# version is released.
|
8
|
+
#
|
9
|
+
# API compatibility is only guaranteed for minor version changes;
|
10
|
+
# New major versions may change the API and require code changes
|
11
|
+
# in projects using this plugin.
|
12
|
+
#
|
13
|
+
# Because of the API stability guarantee, these spec's may only
|
14
|
+
# be changed for new major releases.
|
15
|
+
|
16
|
+
before(:all) do
|
17
|
+
ScopedSearch::Spec::Database.establish_connection
|
18
|
+
end
|
19
|
+
|
20
|
+
after(:all) do
|
21
|
+
ScopedSearch::Spec::Database.close_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'for unprepared ActiveRecord model' do
|
25
|
+
|
26
|
+
it "should respond to :scoped_search to setup scoped_search for the model" do
|
27
|
+
Class.new(ActiveRecord::Base).should respond_to(:scoped_search)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'for a prepared ActiveRecord model' do
|
32
|
+
|
33
|
+
before(:all) do
|
34
|
+
@class = ScopedSearch::Spec::Database.create_model(:field => :string) do |klass|
|
35
|
+
klass.scoped_search :on => :field
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
after(:all) do
|
40
|
+
ScopedSearch::Spec::Database.drop_model(@class)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should respond to :search_for to perform searches" do
|
44
|
+
@class.should respond_to(:search_for)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should return an ActiveRecord::NamedScope::Scope when :search_for is called" do
|
48
|
+
@class.search_for('query').class.should eql(ActiveRecord::NamedScope::Scope)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'having backwards compatibility' do
|
53
|
+
|
54
|
+
before(:each) do
|
55
|
+
class Foo < ActiveRecord::Base
|
56
|
+
belongs_to :bar
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
after(:each) do
|
61
|
+
Object.send :remove_const, :Foo
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should respond to :searchable_on" do
|
65
|
+
Foo.should respond_to(:searchable_on)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should create a Field instance for every argument" do
|
69
|
+
ScopedSearch::Definition::Field.should_receive(:new).exactly(3).times
|
70
|
+
Foo.searchable_on(:field_1, :field_2, :field_3)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should create a Field with a valid relation when using the underscore notation" do
|
74
|
+
ScopedSearch::Definition::Field.should_receive(:new).with(
|
75
|
+
instance_of(ScopedSearch::Definition), hash_including(:in => :bar, :on => :related_field))
|
76
|
+
|
77
|
+
Foo.searchable_on(:bar_related_field)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|