de 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,273 @@
1
+ #
2
+ # De::SunspotSolr operand classes.
3
+ # This file defines:
4
+ # - basic De::SunspotSolr::SunspotOperand class
5
+ # - De::SunspotSolr::IntervalSunspotOperand extending SunspotOperand for case of time intervals
6
+ # - A number of classes extending mentioned below and giving convenient interface for concrete sunspot query operations:
7
+ # - EqualTo, Without, GreaterThan, LessThan, Between, AnyOf, GreaterThanOrEqual, LessThanOrEqual
8
+ # - IntervalFromNowEqualTo, IntervalFromNowWithout, IntervalFromNowGreaterThan, IntervalFromNowLessThan, IntervalFromNowBetween,
9
+ # IntervalFromNowAnyOf, IntervalFromNowGreaterThanOrEqual, IntervalFromNowLessThanOrEqual
10
+ #
11
+ # De::SunspotSolr module provides engine to build, validate and evaluate dynamic sunspot query to solr
12
+ # based on some model.
13
+ # It is built as extension of De module
14
+ #
15
+ # SunspotSolr expression example:
16
+ #
17
+ # Sunspot.search(Product) do
18
+ # any_of do
19
+ # with(:client_id).equal_to(43)
20
+ # with(:name).equal_to('Name to trace')
21
+ # end
22
+ # with(:update_time).greater_then('2011-03-15 15:00:00')
23
+ # end
24
+ #
25
+
26
+ #require 'active_support/inflector'
27
+ require 'active_support/all'
28
+
29
+ module De
30
+ module SunspotSolr
31
+
32
+ # Marker module included to all module classes
33
+ # to be able to check that class is inside module
34
+ module SP; end
35
+
36
+ #
37
+ # Basic class representing sunspot search expression operand
38
+ #
39
+ # Operand representation in Sunspot query examples:
40
+ # - with(:client_id).equal_to(43)
41
+ # - with(:update_time).greater_then('2011-03-15 15:00:00')
42
+ #
43
+ #
44
+ class SunspotOperand < Operand
45
+ include De::SunspotSolr::SP
46
+
47
+ attr_reader :value, :operand
48
+
49
+ #
50
+ # Constructor for SunspotOperand
51
+ # Stores name and property as TreeNode's @name and @content correspondingly
52
+ #
53
+ # === Input
54
+ #
55
+ # name<String|Symbol>:: arbitrary object name
56
+ # property<String|Symbol>:: property examined by sunspot query
57
+ # (for example, :client_id in condition with(:client_id).equal_to(43)
58
+ # Stored as TreeNode's @content field
59
+ # operand<Symbol>:: operand type
60
+ # Examples: :equal_to, :without, :greater_than...
61
+ # value<Object>:: value to compare
62
+ # Example: 43 in condition with(:client_id).equal_to(43)
63
+ #
64
+ def initialize(name, property, operand, value)
65
+ super(name.to_s, property.to_s)
66
+ @value = value
67
+ @operand = operand
68
+ end
69
+
70
+ #
71
+ # Validation
72
+ #
73
+ # Operand is valid in case
74
+ # - its operand is included to valid_operands list
75
+ # - it is added to De::SunspotSolr::Search object and property is registered in this root object
76
+ # Last one is needed to choose appropriate condition representation according to property type and static/dynamic nature
77
+ #
78
+ def valid?
79
+ return false unless self.class.valid_operands.include?(@operand)
80
+ return false unless root.is_a?(Search)
81
+ return false unless root.options[:properties].key?(@content.to_sym)
82
+ true
83
+ end
84
+
85
+ #
86
+ # Evaluation
87
+ #
88
+ # Result depends on property type and static/dynamic nature
89
+ #
90
+ # === Output
91
+ #
92
+ # <String> - string representation of sunspot condition
93
+ #
94
+ def evaluate
95
+ super
96
+ if root.options[:properties][@content.to_sym][:dynamic]
97
+ "dynamic(:#{root.options[:dynamics][root.options[:properties][@content.to_sym][:type]]}) do
98
+ #{simple_evaluate}
99
+ end"
100
+ else
101
+ simple_evaluate
102
+ end
103
+ end
104
+
105
+ #
106
+ # Equal operand override
107
+ # Checks objects class and coincidence of +operand+, +value+ and +property+ (stored in +content+ filed)
108
+ #
109
+ # === Input
110
+ #
111
+ # obj<SunspotOperator>:: object to compare with
112
+ #
113
+ def ==(obj)
114
+ obj.is_a?(SunspotOperand) && obj.operand == @operand && obj.content == @content && obj.value == @value
115
+ end
116
+
117
+ #
118
+ # Define hash function to give the same result for equal @operand, @content and @value
119
+ # (@name is not important for equal operands)
120
+ #
121
+ def hash
122
+ [@operand, @content, @value].hash
123
+ end
124
+
125
+ class << self
126
+
127
+ #
128
+ # Supported operands list
129
+ #
130
+ # === Output
131
+ #
132
+ # <Array> of symbols
133
+ #
134
+ def valid_operands
135
+ [:equal_to, :greater_than, :less_than, :between, :any_of]
136
+ end
137
+ end
138
+
139
+ protected
140
+
141
+ def simple_evaluate
142
+ "with(:#{@content}).#{@operand}(#{value_to_compare})"
143
+ end
144
+
145
+ def value_to_compare
146
+ if @value.is_a?(Array)
147
+ output = "[#{@value.map {|element| atomic_value(element) }.join(',')}]"
148
+ elsif @value.is_a?(Range)
149
+ output = @value.exclude_end? ? atomic_value(@value.first)...atomic_value(@value.last) : atomic_value(@value.first)..atomic_value(@value.last)
150
+ else
151
+ output = atomic_value(@value)
152
+ end
153
+
154
+ output
155
+ end
156
+
157
+ def atomic_value(val)
158
+ case root.options[:properties][@content.to_sym][:type]
159
+ when :text, :string
160
+ "'#{val.escape_apos}'"
161
+ when :time
162
+ "'#{val.to_s(:db)}'"
163
+ when :float
164
+ val.to_f
165
+ else
166
+ val
167
+ end
168
+ end
169
+ end
170
+
171
+ #
172
+ # Sunspot interval operand class
173
+ #
174
+ # It expends SunspotOperand class for properties of type :time
175
+ # and evaluates their values compared to given by interval from now.
176
+ # Interval is considered in days
177
+ #
178
+ # For example
179
+ #
180
+ # IntervalSunspotOperand.new('op1', :start_date, :less_then, 3)
181
+ #
182
+ # gives condition
183
+ #
184
+ # with(:start_date).less_than(Time.now + 3.days)
185
+ #
186
+ class IntervalSunspotOperand < SunspotOperand
187
+
188
+ def valid?
189
+ super && root.options[:properties][@content.to_sym] && root.options[:properties][@content.to_sym][:type] == :time
190
+ end
191
+
192
+ protected
193
+
194
+ def value_to_compare
195
+ now = Time.now
196
+ if @value.is_a?(Array)
197
+ shifted_value = "[#{@value.map {|element| atomic_value(now + element.days) }.join(',')}]"
198
+ elsif @value.is_a?(Range)
199
+ shifted_value = @value.exclude_end? ?
200
+ "#{atomic_value(now + @value.first.days)}...#{atomic_value( now + @value.last.days)}" :
201
+ "#{atomic_value(now + @value.first.days)}..#{atomic_value( now + @value.last.days)}"
202
+ else
203
+ shifted_value = atomic_value(now + @value.days)
204
+ end
205
+
206
+ shifted_value
207
+ end
208
+ end
209
+
210
+ available_operands = SunspotOperand.valid_operands | [:greater_than_or_equal, :less_than_or_equal, :without]
211
+ available_operands.each do |operand|
212
+ module_eval "
213
+ class #{operand.to_s.camelize} < SunspotOperand
214
+ def initialize(property, value)
215
+ super(\"\#\{property\}-\#\{rand(1000)\}\", property, :#{operand}, value)
216
+ end
217
+
218
+ def self.valid_operands
219
+ [:#{operand}]
220
+ end
221
+ end
222
+
223
+ class IntervalFromNow#{operand.to_s.camelize} < IntervalSunspotOperand
224
+ def initialize(property, value)
225
+ super(\"\#\{property\}-\#\{rand(1000)\}\", property, :#{operand}, value)
226
+ end
227
+
228
+ def self.valid_operands
229
+ [:#{operand}]
230
+ end
231
+ end
232
+ "
233
+ end
234
+
235
+ [:greater_than_or_equal, :less_than_or_equal].each do |operand|
236
+ module_eval %{
237
+ class #{operand.to_s.camelize} < SunspotOperand
238
+
239
+ def simple_evaluate
240
+ "any_of do
241
+ with(:\#\{@content\}).#{operand.to_s.gsub(/_or_equal/, '')}(\#\{value_to_compare\})
242
+ with(:\#\{@content\}, \#\{value_to_compare\})
243
+ end"
244
+ end
245
+ end
246
+
247
+ class IntervalFromNow#{operand.to_s.camelize} < IntervalSunspotOperand
248
+
249
+ def simple_evaluate
250
+ "any_of do
251
+ with(:\#\{@content\}).#{operand.to_s.gsub(/_or_equal/, '')}(\#\{value_to_compare\})
252
+ with(:\#\{@content\}, \#\{value_to_compare\})
253
+ end"
254
+ end
255
+ end
256
+ }
257
+ end
258
+
259
+ class Without < SunspotOperand
260
+
261
+ def simple_evaluate
262
+ "without(:#{@content}, #{value_to_compare})"
263
+ end
264
+ end
265
+
266
+ class IntervalFromNowWithout < IntervalSunspotOperand
267
+
268
+ def simple_evaluate
269
+ "without(:#{@content}, #{value_to_compare})"
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,131 @@
1
+ #
2
+ # De::SunspotSolr operator classes: And and Or
3
+ #
4
+ # De::SunspotSolr module provides engine to build, validate and evaluate dynamic sunspot query to solr
5
+ # based on some model.
6
+ # It is built as extension of De module
7
+ #
8
+ # SunspotSolr expression example:
9
+ #
10
+ # Sunspot.search(Product) do
11
+ # any_of do
12
+ # with(:client_id).equal_to(43)
13
+ # with(:name).equal_to('Name to trace')
14
+ # end
15
+ # with(:update_time).greater_then('2011-03-15 00:15:00')
16
+ # end
17
+ #
18
+
19
+ require 'de/symmetric_operator'
20
+
21
+ module De
22
+ module SunspotSolr
23
+
24
+ # Marker module included to all module classes
25
+ # to be able to check that class is inside module
26
+ module SP; end
27
+
28
+ class SunspotOperator < Operator
29
+ include De::SunspotSolr::SP
30
+ include De::SymmetricOperator
31
+
32
+ def initialize(operator, operands = nil)
33
+ @operator = operator
34
+ super("#{operator}-#{rand(1000)}", operands)
35
+ end
36
+
37
+ def evaluate
38
+ super
39
+ "#{@operator} do
40
+ #{children.map {|child| child.evaluate + "\n" } }
41
+ end"
42
+ end
43
+
44
+ #
45
+ # Adds operator or operand as a child in case equal one doesn't exist already
46
+ # otherwize old one is returned
47
+ #
48
+ # === Input
49
+ #
50
+ # obj<SP>:: object to be added as child one
51
+ #
52
+ # === Output
53
+ #
54
+ # <SP>:: child object
55
+ #
56
+ def <<(obj)
57
+ raise Error::TypeError unless obj.is_a?(SP)
58
+ children.include?(obj) ? children[children.index(obj)] : super(obj)
59
+ end
60
+ end
61
+
62
+ #
63
+ # Class representing AND operator for SunspotSolr expression.
64
+ # In resulting Sunspot query it is reflected as all_of block
65
+ #
66
+ class And < SunspotOperator
67
+
68
+ #
69
+ # Creates And object.
70
+ # Name includes word 'all_of' with random number to avoid TreeNode problem
71
+ # when trying to add children with the same name to a node
72
+ #
73
+ # === Input
74
+ #
75
+ # operands<Array>:: (optional) array of Operand objects.
76
+ # If given they are added as children to current operator
77
+ #
78
+ def initialize(operands = nil)
79
+ super("all_of", operands)
80
+ end
81
+ end
82
+
83
+ class Or < SunspotOperator
84
+
85
+ #
86
+ # Creates Or object.
87
+ # Name includes word 'any_of' with random number to avoid TreeNode problem
88
+ # when trying to add children with the same name to a node
89
+ #
90
+ # === Input
91
+ #
92
+ # operands<Array>:: (optional) array of Operand objects.
93
+ # If given they are added as children to current operator
94
+ #
95
+ def initialize(operands = nil)
96
+ super("any_of", operands)
97
+ end
98
+ end
99
+
100
+ class Not < SunspotOperator
101
+
102
+ #
103
+ # Creates Or object.
104
+ # Name includes word 'any_of' with random number to avoid TreeNode problem
105
+ # when trying to add children with the same name to a node
106
+ #
107
+ # === Input
108
+ #
109
+ # operands<Array>:: (optional) array of Operand objects.
110
+ # If given they are added as children to current operator
111
+ #
112
+ def initialize(operand = nil)
113
+ super("not", operand ? [operand] : nil)
114
+ end
115
+
116
+ #
117
+ # Adds sunspot operand as a child to not operator
118
+ #
119
+ def <<(obj)
120
+ raise Error::TypeError unless obj.is_a?(De::SunspotSolr::SunspotOperand)
121
+ raise Error::ArgumentNumerError if has_children?
122
+ super(obj)
123
+ end
124
+
125
+ def evaluate
126
+ super
127
+ first_child.evaluate.gsub(/with\(/, 'without(')
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,179 @@
1
+ #
2
+ # De::SunspotSolr::Search class
3
+ #
4
+ # De::SunspotSolr module provides engine to build, validate and evaluate dynamic sunspot query to solr
5
+ # based on some model.
6
+ # It is built as extension of De module
7
+ #
8
+ # SunspotSolr expression example:
9
+ #
10
+ # Sunspot.search(Product) do
11
+ # any_of do
12
+ # with(:client_id).equal_to(43)
13
+ # with(:name).equal_to('Name to trace')
14
+ # end
15
+ # with(:update_time).greater_then('15/03/11')
16
+ # end
17
+ #
18
+
19
+ require 'de/symmetric_operator'
20
+
21
+ module De
22
+ module SunspotSolr
23
+
24
+ # Marker module included to all module classes
25
+ # to be able to check that class is inside module
26
+ module SP; end
27
+
28
+ #
29
+ # Search class
30
+ #
31
+ # Expression extention representing sunspot search
32
+ # Its evaluation returns Sunspot Search object
33
+ # built on its operators and operands tree
34
+ #
35
+ class Search < Expression
36
+ include SP
37
+ include De::SymmetricOperator
38
+
39
+ attr_reader :klass, :options
40
+
41
+ #
42
+ # Constructor
43
+ # Stores object properties. Adds operands to expression tree
44
+ #
45
+ # === Input
46
+ #
47
+ # name<String>:: arbitrary object name
48
+ # klass<String>:: name of model search is built on
49
+ # options<Hash>:: options specifying model fields properties as they are registered in Solr
50
+ # Options are used by operands in order to build proper sunspot condition according to property type and static/dynamic nature
51
+ #
52
+ # Hash has following structure
53
+ # {
54
+ # :properties => { <property> => {:type => <type>, :dynamic => <dynamic>}, ... },
55
+ # :dynamics => { <type> => <dynamic property name>, ... }
56
+ # }
57
+ #
58
+ # <type> - type name (symbol)
59
+ # <dynamic> - true|false
60
+ # <dynamic property name> - dynamic solr field name (symbol)
61
+ #
62
+ # operands<Array>:: (optional) array of Operand objects.
63
+ # If given they are added as children to current Search object
64
+ #
65
+ # === Exmaple
66
+ # search = De::SunspotSolr::Search.new('search_product', 'Product', {
67
+ # :properties => {
68
+ # :client_id => {:type => :integer, :dynamic => false},
69
+ # :name => {:type => :string, :dynamic => false},
70
+ # :price => {:type => :integer, :dynamic => true}
71
+ # },
72
+ # :dynamics => {:integer => :int_params, :string => :string_params, :time => :time_params, :text => :string_params}
73
+ # })
74
+ #
75
+ def initialize(name, klass, options = {}, operands = nil)
76
+ super(name, nil)
77
+ @klass = klass
78
+ @options = options
79
+
80
+ unless operands.nil?
81
+ raise Error::TypeError unless operands.is_a?(Array)
82
+ operands.each { |operand| self << operand }
83
+ end
84
+ end
85
+
86
+ #
87
+ # Adds operator or operand as a child to Search.
88
+ # Prevents addition of invalid type Object
89
+ #
90
+ # === Input
91
+ #
92
+ # obj<SunspotSolr::Operator|SunspotSolr::Operand>:: object to be added as child one
93
+ #
94
+ # === Output
95
+ #
96
+ # <SunspotSolr::Operator|SunspotSolr::Operand>:: child object
97
+ #
98
+ def <<(obj)
99
+ raise Error::TypeError unless (obj.is_a?(SP))
100
+ children.include?(obj) ? children[children.index(obj)] : super(obj)
101
+ end
102
+
103
+ #
104
+ # Equal override
105
+ # Checks objects class, +klass+ property and children equality. Children order is NOT important
106
+ #
107
+ # === Input
108
+ #
109
+ # obj<SunspotSolr::Search>:: object to compare with
110
+ #
111
+ def ==(obj)
112
+ obj.is_a?(Search) && obj.klass == @klass && ((children | obj.children) - (children & obj.children)).length == 0
113
+ end
114
+
115
+ #
116
+ # Intersection operator. Reterns new Search object representing intersection of results
117
+ # given by current and input search results
118
+ #
119
+ # === Input
120
+ #
121
+ # obj<Search>:: search to find intersection with
122
+ #
123
+ # === Output
124
+ #
125
+ # Search object
126
+ #
127
+ def &(obj)
128
+ Search.new("#{@name}+#{obj.name}", @klass, @options, [self.children, obj.children].flatten)
129
+ end
130
+
131
+ #
132
+ # Union operator. Reterns new Search object representing unin of results
133
+ # given by current and input search results
134
+ #
135
+ # === Input
136
+ #
137
+ # obj<Search>:: search to find union with
138
+ #
139
+ # === Output
140
+ #
141
+ # Search object
142
+ #
143
+ def |(obj)
144
+ expression_content = []
145
+ expression_content << SunspotSolr::And.new(self.children) if self.has_children?
146
+ expression_content << SunspotSolr::And.new(obj.children) if obj.has_children?
147
+
148
+ Search.new("#{@name}+#{obj.name}", @klass, @options, expression_content.length > 0 ? [SunspotSolr::Or.new(expression_content)] : nil)
149
+ end
150
+
151
+ #
152
+ # Validator
153
+ #
154
+ # Expression tree is valid in case their children are valid if any
155
+ #
156
+ def valid?
157
+ children.inject(true) {|result, el| result && el.valid?}
158
+ end
159
+
160
+ #
161
+ # Evaluator
162
+ #
163
+ # === Output
164
+ #
165
+ # Sunspot Search object
166
+ # built on its current tree operators and operands
167
+ #
168
+ def evaluate
169
+ super
170
+
171
+ search_string = "Sunspot.search(#{Kernel.const_get(@klass)}) do
172
+ #{children.map {|child| child.evaluate + "\n" } }
173
+ end"
174
+
175
+ instance_eval search_string
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,9 @@
1
+ require 'de/sunspot_solr/operand'
2
+ require 'de/sunspot_solr/operator'
3
+ require 'de/sunspot_solr/search'
4
+
5
+ class String
6
+ def escape_apos
7
+ self.gsub(/'/, "\\\\'")
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ #
2
+ # Symmetric operator module defines properties for operator with unimportant order of children (operands)
3
+ #
4
+
5
+ require 'set'
6
+
7
+ module De
8
+ module SymmetricOperator
9
+
10
+ #
11
+ # Equal operator override
12
+ # Checks objects class and children equality. Operands order is NOT important
13
+ #
14
+ # === Input
15
+ #
16
+ # obj<Expression>:: object to compare with
17
+ #
18
+ def ==(obj)
19
+ self.class.name == obj.class.name && ((children | obj.children) - (children & obj.children)).length == 0
20
+ end
21
+
22
+ #
23
+ # Define hash function to get equal results for operators from the same class and with the equal children.
24
+ # Children order is not important
25
+ #
26
+ def hash
27
+ [self.class.name, Set.new(children.map { |el| el.hash})].hash
28
+ end
29
+
30
+ end
31
+ end
data/lib/de/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module De
2
+ VERSION = "0.0.1"
3
+ end
data/lib/de.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'de/version'
2
+ require 'de/de'
3
+
@@ -0,0 +1,51 @@
1
+ require 'test/unit'
2
+ require 'de'
3
+ require 'de/boolean'
4
+
5
+ class DeBooleanAndTest < Test::Unit::TestCase
6
+
7
+ def test_constructor
8
+ assert_nothing_raised(De::Error::AbstractClassObjectCreationError) { De::Boolean::And.new }
9
+ assert_nothing_raised(De::Error::AbstractClassObjectCreationError) { De::Boolean::And.new([De::Boolean::Operand.new('some name', true)]) }
10
+ end
11
+
12
+ def test_add
13
+ and1 = De::Boolean::And.new
14
+ operand1 = De::Boolean::Operand.new('some name', true)
15
+
16
+ and1 << operand1
17
+ assert_equal(2, and1.size)
18
+
19
+ and2 = De::Boolean::And.new
20
+ and1 << and2
21
+ assert_equal(3, and1.size)
22
+ end
23
+
24
+ def test_evaluate
25
+ and1 = De::Boolean::And.new
26
+ operand_true1 = De::Boolean::Operand.new('some name', true)
27
+ operand_true2 = De::Boolean::Operand.new('second name', true)
28
+ operand_false1 = De::Boolean::Operand.new('third name', false)
29
+ operand_false2 = De::Boolean::Operand.new('fourth name', false)
30
+
31
+ and1 << operand_true1
32
+ and1 << operand_true2
33
+ assert_equal(true, and1.evaluate)
34
+
35
+ and2 = De::Boolean::And.new
36
+ and2 << operand_true1
37
+ and2 << operand_false1
38
+ assert_equal(false, and2.evaluate)
39
+
40
+ and3 = De::Boolean::And.new([operand_false1, operand_false2])
41
+ assert_equal(false, and3.evaluate)
42
+
43
+ and1 << and3
44
+ assert_equal(false, and1.evaluate)
45
+
46
+ and4 = De::Boolean::And.new([operand_true1, operand_true2])
47
+ and1 << and4
48
+ assert_equal(false, and1.evaluate)
49
+ end
50
+
51
+ end