de 0.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,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