tinia 0.0.3

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