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