tinia 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ module Tinia
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer "tinia.activate" do
5
+ Tinia.activate_active_record!
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,86 @@
1
+ module Tinia
2
+
3
+ module Search
4
+
5
+ def self.included(klass)
6
+ klass.send(:extend, ClassMethods)
7
+ klass.class_eval do
8
+ # lambda block for the scope
9
+ scope_def = lambda{|*ids|
10
+ {
11
+ :conditions => [
12
+ "#{self.table_name}.#{self.primary_key} IN (?)", ids.flatten
13
+ ]
14
+ }
15
+ }
16
+ named_scope :tinia_scope, scope_def do
17
+ include WillPaginateMethods
18
+ end
19
+ end
20
+ end
21
+
22
+ # methods of WillPaginate compatibility
23
+ module WillPaginateMethods
24
+
25
+ attr_accessor :current_page, :per_page, :total_entries
26
+
27
+ # calculated offset given the current page and the number
28
+ # of entries per page
29
+ def offset
30
+ (self.current_page - 1) * self.per_page
31
+ end
32
+
33
+ # total number of pages
34
+ def total_pages
35
+ (self.total_entries.to_f / self.per_page.to_f).ceil
36
+ end
37
+
38
+ end
39
+
40
+ module ClassMethods
41
+
42
+ # return a scope with the subset of ids
43
+ def cloud_search(*args)
44
+ opts = {
45
+ :page => 1,
46
+ :per_page => 20
47
+ }
48
+ opts = opts.merge(args.extract_options!)
49
+ query = args.first
50
+
51
+ response = self.cloud_search_response(args.first, opts)
52
+ ids = response.hits.collect{|h| h["id"]}
53
+
54
+ proxy = self.tinia_scope(ids)
55
+ proxy.per_page = opts[:per_page]
56
+ proxy.current_page = opts[:page]
57
+ proxy.total_entries = response.found
58
+ proxy
59
+ end
60
+
61
+ protected
62
+
63
+ # perform a query to CloudSearch and get a response
64
+ def cloud_search_response(query, opts)
65
+ self.cloud_search_connection.search(
66
+ self.cloud_search_request(query, opts)
67
+ )
68
+ end
69
+
70
+ # generate a request to CloudSearch
71
+ def cloud_search_request(query, opts)
72
+ AWSCloudSearch::SearchRequest.new.tap do |req|
73
+ if query.nil?
74
+ req.bq = "type:'#{self.base_class.name}'"
75
+ else
76
+ req.bq = "(and '#{query}' type:'#{self.base_class.name}')"
77
+ end
78
+ req.size = opts[:per_page]
79
+ req.start = (opts[:page] - 1) * opts[:per_page]
80
+ end
81
+ end
82
+
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe "AWS" do
4
+
5
+ before(:all) do
6
+
7
+ conn = ActiveRecord::Base.connection
8
+ conn.create_table(:clients, :force => true) do |t|
9
+ t.string(:first_name)
10
+ t.string(:last_name)
11
+ t.string(:email)
12
+ t.timestamps
13
+ end
14
+
15
+ Client = Class.new(ActiveRecord::Base) do
16
+ indexed_with_cloud_search do |config|
17
+ config.cloud_search_domain = "client-4wwi2n4ghrnro46w2adiw2temy"
18
+ end
19
+
20
+ def cloud_search_data
21
+ self.attributes
22
+ end
23
+
24
+ end
25
+
26
+ Client.create(
27
+ :first_name => "Dan",
28
+ :last_name => "Langevin",
29
+ :email => "test@test.com"
30
+ )
31
+ end
32
+
33
+ after(:all) do
34
+ Client.all.each(&:destroy)
35
+ end
36
+
37
+ it "should be able to index and search records" do
38
+
39
+ Client.cloud_search_reindex
40
+ c = Client.cloud_search("Dan Langevin").first
41
+ c.should be_instance_of(Client)
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'tinia'
5
+ require 'active_record'
6
+ require 'mocha'
7
+
8
+ require 'ruby-debug'
9
+ Debugger.start
10
+
11
+ # Requires supporting files with custom matchers and macros, etc,
12
+ # in ./support/ and its subdirectories.
13
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
14
+
15
+ ActiveRecord::Base.establish_connection({
16
+ "adapter" => "sqlite3",
17
+ "database" => "/tmp/tinia_test.sqlite"
18
+ })
19
+
20
+ Tinia.activate_active_record!
21
+
22
+ Mocha::Configuration.prevent(:stubbing_non_existent_method)
23
+
24
+ RSpec.configure do |config|
25
+ config.mock_with :mocha
26
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tinia::Connection do
4
+
5
+ before(:each) do
6
+
7
+ ConnectionMock = Class.new(ActiveRecord::Base) do
8
+ indexed_with_cloud_search do |config|
9
+ config.cloud_search_domain = "connection-mock"
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ context ".cloud_search_connection" do
16
+
17
+ it "should get a namespaced connection" do
18
+ ConnectionMock.stubs(:cloud_search_domain => "connection-mock")
19
+ Tinia.expects(:connection).with("connection-mock")
20
+ ConnectionMock.cloud_search_connection
21
+ end
22
+
23
+ end
24
+
25
+ context ".cloud_search_domain" do
26
+
27
+ it "should provide a setter and getter" do
28
+ ConnectionMock.cloud_search_domain = "123"
29
+ ConnectionMock.cloud_search_domain.should eql "123"
30
+
31
+ ConnectionMock2 = Class.new(ConnectionMock)
32
+ ConnectionMock2.cloud_search_domain.should eql "123"
33
+ end
34
+
35
+ it "should raise an error if cloud_search_domain is not defined" do
36
+
37
+ lambda{
38
+ ErrorMock = Class.new(ActiveRecord::Base) do
39
+ indexed_with_cloud_search
40
+ end
41
+ }.should raise_error(Tinia::MissingSearchDomain)
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,197 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tinia::Index do
4
+
5
+ before(:all) do
6
+
7
+ conn = ActiveRecord::Base.connection
8
+
9
+ conn.create_table(:mock_index_classes, :force => true) do |t|
10
+ t.string(:name)
11
+ t.timestamps
12
+ end
13
+
14
+ conn.create_table(:mock_index_with_datas, :force => true) do |t|
15
+ t.string(:name)
16
+ t.string(:type)
17
+ t.timestamps
18
+ end
19
+
20
+ MockIndexClass = Class.new(ActiveRecord::Base) do
21
+ indexed_with_cloud_search do |config|
22
+ config.cloud_search_domain = "connection-mock"
23
+ end
24
+ end
25
+
26
+ MockIndexWithData = Class.new(ActiveRecord::Base) do
27
+ indexed_with_cloud_search do |config|
28
+ config.cloud_search_domain = "mock-index-with-data"
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ shared_examples_for "batching" do
35
+
36
+ it "should add a document to its batch, flushing each time" do
37
+
38
+ doc = AWSCloudSearch::Document.new
39
+ doc2 = AWSCloudSearch::Document.new
40
+
41
+ batcher = AWSCloudSearch::DocumentBatcher.new(stub)
42
+ batcher.expects(document_method).with(doc)
43
+ batcher.expects(document_method).with(doc2)
44
+ batcher.expects(:flush).twice
45
+
46
+ MockIndexWithData.stubs(:cloud_search_document_batcher => batcher)
47
+
48
+ MockIndexWithData.send(batch_method, doc)
49
+ MockIndexWithData.send(batch_method, doc2)
50
+
51
+ end
52
+
53
+ it "should add a document to its batch flushing only at the end
54
+ when within a cloud_search_batch_documents block" do
55
+
56
+
57
+ doc = AWSCloudSearch::Document.new
58
+ doc2 = AWSCloudSearch::Document.new
59
+
60
+ batcher = AWSCloudSearch::DocumentBatcher.new(stub)
61
+ batcher.expects(document_method).with(doc)
62
+ batcher.expects(document_method).with(doc2)
63
+
64
+ # exactly one
65
+ batcher.expects(:flush).once
66
+
67
+ MockIndexWithData.stubs(:cloud_search_document_batcher => batcher)
68
+
69
+ MockIndexWithData.cloud_search_batch_documents do
70
+ MockIndexWithData.send(batch_method, doc)
71
+ MockIndexWithData.send(batch_method, doc2)
72
+ end
73
+
74
+ end
75
+ end
76
+
77
+ context ".cloud_search_add_document" do
78
+
79
+ let(:batch_method) do
80
+ :cloud_search_add_document
81
+ end
82
+
83
+ let(:document_method) do
84
+ :add_document
85
+ end
86
+
87
+ it_should_behave_like("batching")
88
+ end
89
+
90
+ context ".cloud_search_delete_document" do
91
+
92
+ let(:batch_method) do
93
+ :cloud_search_delete_document
94
+ end
95
+
96
+ let(:document_method) do
97
+ :delete_document
98
+ end
99
+
100
+ it_should_behave_like("batching")
101
+ end
102
+
103
+ context ".cloud_search_domain" do
104
+
105
+ it "should be an inheritable attribute" do
106
+ NewKlass = Class.new(MockIndexWithData)
107
+ NewKlass.cloud_search_domain.should eql(
108
+ MockIndexWithData.cloud_search_domain
109
+ )
110
+ end
111
+
112
+ end
113
+
114
+
115
+ context ".cloud_search_reindex" do
116
+
117
+ it "should reindex the entire collection" do
118
+ mock_index_with_data = MockIndexWithData.new
119
+ mock_index_with_data.expects(:add_to_cloud_search)
120
+
121
+ MockIndexWithData.cloud_search_document_batcher.expects(:flush)
122
+ MockIndexWithData.expects(:find_each)
123
+ .with(:conditions => ["x = y"])
124
+ .yields(mock_index_with_data)
125
+
126
+ MockIndexWithData.cloud_search_reindex(:conditions => ["x = y"])
127
+ end
128
+
129
+ end
130
+
131
+
132
+ context "#add_to_cloud_search" do
133
+
134
+ it "should add a document to CloudSearch" do
135
+ doc = AWSCloudSearch::Document.new
136
+
137
+ mock_index_with_data = MockIndexWithData.new
138
+ mock_index_with_data.stubs(:cloud_search_document => doc)
139
+
140
+ MockIndexWithData.expects(:cloud_search_add_document).with(doc)
141
+
142
+ # call add to cloud_search
143
+ mock_index_with_data.add_to_cloud_search
144
+ end
145
+ end
146
+
147
+ context "#cloud_search_data" do
148
+
149
+ it "should define an empty cloud_search_data method" do
150
+ MockIndexClass.new.cloud_search_data.should eql({})
151
+ end
152
+
153
+ end
154
+
155
+ context "#cloud_search_document" do
156
+
157
+ it "should provide a wrapper for the document that is indexed " do
158
+
159
+ t = Time.now
160
+
161
+ doc = AWSCloudSearch::Document.new
162
+ doc.expects(:id=).with(8989)
163
+ doc.expects(:lang=).with("en")
164
+ doc.expects(:version=).with(t.to_i)
165
+ doc.expects(:add_field).with("key", "val")
166
+ doc.expects(:add_field).with("type", "MockIndexWithData")
167
+ AWSCloudSearch::Document.stubs(:new => doc)
168
+
169
+ mock_index_with_data = MockIndexWithData.new
170
+ mock_index_with_data.stubs(
171
+ :cloud_search_data => {:key => "val"},
172
+ :id => 8989,
173
+ :updated_at => t.to_i
174
+ )
175
+ mock_index_with_data.cloud_search_document.should eql(doc)
176
+ end
177
+
178
+ end
179
+
180
+ context "#delete_from_cloud_search" do
181
+
182
+ it "should remvoe a document from CloudSearch" do
183
+ doc = AWSCloudSearch::Document.new
184
+
185
+ mock_index_with_data = MockIndexWithData.new
186
+ mock_index_with_data.stubs(:cloud_search_document => doc)
187
+
188
+ MockIndexWithData.expects(:cloud_search_delete_document).with(doc)
189
+
190
+ # call add to cloud_search
191
+ mock_index_with_data.delete_from_cloud_search
192
+ end
193
+
194
+ end
195
+
196
+
197
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tinia::Search do
4
+
5
+ before(:all) do
6
+
7
+ conn = ActiveRecord::Base.connection
8
+ conn.create_table(:mock_classes, :force => true) do |t|
9
+ t.string("name")
10
+ t.timestamps
11
+ end
12
+
13
+ MockClass = Class.new(ActiveRecord::Base) do
14
+ indexed_with_cloud_search do |config|
15
+ config.cloud_search_domain = "mock-class"
16
+ end
17
+
18
+ named_scope :name_like, lambda{|n|
19
+ {:conditions => ["name LIKE ?", n]}
20
+ }
21
+
22
+ end
23
+
24
+ end
25
+
26
+ context "#cloud_search" do
27
+
28
+ before(:each) do
29
+ AWSCloudSearch::SearchRequest.stubs(:new => search_request)
30
+
31
+ MockClass.cloud_search_connection
32
+ .expects(:search)
33
+ .with(search_request)
34
+ .returns(stub({
35
+ :hits => [
36
+ {"id" => 1},
37
+ {"id" => 2}
38
+ ],
39
+ :found => 300,
40
+ :start => 0
41
+ }))
42
+ end
43
+
44
+ let(:search_request) do
45
+ search_request = AWSCloudSearch::SearchRequest.new
46
+ search_request.expects(:bq=).with("(and 'my query' type:'MockClass')")
47
+ search_request
48
+ end
49
+
50
+ it "should proxy its search request to cloud_search and return
51
+ an Arel-like object" do
52
+
53
+ proxy = MockClass.cloud_search("my query")
54
+ proxy.proxy_options[:conditions].should eql(
55
+ ["mock_classes.id IN (?)", [1, 2]]
56
+ )
57
+ end
58
+
59
+ it "should be chainable, maintaining its meta_data" do
60
+ proxy = MockClass.cloud_search("my query").name_like("name")
61
+ proxy.current_page.should eql(1)
62
+ proxy.offset.should eql(0)
63
+ end
64
+
65
+ context "#current_page" do
66
+
67
+ it "should default to 1" do
68
+ proxy = MockClass.cloud_search("my query")
69
+ proxy.current_page.should eql(1)
70
+ end
71
+
72
+ it "should be able to be set" do
73
+ search_request.expects(:start=).with(80)
74
+ proxy = MockClass.cloud_search("my query", :page => 5)
75
+ proxy.current_page.should eql(5)
76
+ end
77
+
78
+ end
79
+
80
+ context "#offset" do
81
+
82
+ it "should be able to compute its offset" do
83
+ proxy = MockClass.cloud_search("my query", :page => 5)
84
+ proxy.offset.should eql(80)
85
+ end
86
+
87
+ end
88
+
89
+ context "#per_page" do
90
+
91
+ it "should default to 20" do
92
+ proxy = MockClass.cloud_search("my query")
93
+ proxy.per_page.should eql(20)
94
+ end
95
+
96
+ it "should be able to be set" do
97
+ search_request.expects(:size=).with(50)
98
+ proxy = MockClass.cloud_search("my query", :per_page => 50)
99
+ proxy.per_page.should eql(50)
100
+ end
101
+
102
+ end
103
+
104
+ context "#total_entries" do
105
+
106
+ it "should get it from its search_response" do
107
+ proxy = MockClass.cloud_search("my query")
108
+ proxy.total_entries.should eql(300)
109
+ end
110
+
111
+ end
112
+
113
+ context "#total_pages" do
114
+
115
+ it "should be the ceiling of its total_entries divided
116
+ by per_page" do
117
+ proxy = MockClass.cloud_search("my query", :per_page => 7)
118
+ proxy.total_pages.should eql(43)
119
+ end
120
+
121
+ end
122
+ end
123
+
124
+ end