eden_cloud_search 0.1.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +13 -0
  5. data/Guardfile +10 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +142 -0
  8. data/Rakefile +9 -0
  9. data/eden_cloud_search.gemspec +30 -0
  10. data/lib/eden_cloud_search/config.rb +62 -0
  11. data/lib/eden_cloud_search/document.rb +95 -0
  12. data/lib/eden_cloud_search/exceptions.rb +3 -0
  13. data/lib/eden_cloud_search/indexer.rb +44 -0
  14. data/lib/eden_cloud_search/invalid_document.rb +11 -0
  15. data/lib/eden_cloud_search/search_response.rb +73 -0
  16. data/lib/eden_cloud_search/searcher.rb +118 -0
  17. data/lib/eden_cloud_search/version.rb +3 -0
  18. data/lib/eden_cloud_search.rb +24 -0
  19. data/spec/cloud_search/config_spec.rb +23 -0
  20. data/spec/cloud_search/document_spec.rb +335 -0
  21. data/spec/cloud_search/indexer_spec.rb +146 -0
  22. data/spec/cloud_search/invalid_document_spec.rb +9 -0
  23. data/spec/cloud_search/search_response_spec.rb +220 -0
  24. data/spec/cloud_search/searcher_spec.rb +254 -0
  25. data/spec/fixtures/full.json +1 -0
  26. data/spec/fixtures/vcr_cassettes/index/request/add.yml +38 -0
  27. data/spec/fixtures/vcr_cassettes/index/request/add_in_batch.yml +40 -0
  28. data/spec/fixtures/vcr_cassettes/index/request/delete.yml +37 -0
  29. data/spec/fixtures/vcr_cassettes/search/request/facets.yml +121 -0
  30. data/spec/fixtures/vcr_cassettes/search/request/facets_with_constraints.yml +123 -0
  31. data/spec/fixtures/vcr_cassettes/search/request/full.yml +114 -0
  32. data/spec/fixtures/vcr_cassettes/search/request/paginated.yml +36 -0
  33. data/spec/fixtures/vcr_cassettes/search/request/paginated_first_page.yml +56 -0
  34. data/spec/fixtures/vcr_cassettes/search/request/paginated_second_page.yml +81 -0
  35. data/spec/spec_helper.rb +17 -0
  36. data/spec/support/vcr.rb +5 -0
  37. metadata +212 -0
@@ -0,0 +1,24 @@
1
+ # require "em-http"
2
+ require "rest_client"
3
+ require "json"
4
+ require "eden_cloud_search/version"
5
+
6
+ module CloudSearch
7
+ autoload :Config, "eden_cloud_search/config"
8
+ autoload :ConfigurationChecking, "eden_cloud_search/config"
9
+ autoload :Searcher, "eden_cloud_search/searcher"
10
+ autoload :SearchResponse, "eden_cloud_search/search_response"
11
+ autoload :Indexer, "eden_cloud_search/indexer"
12
+ autoload :Document, "eden_cloud_search/document"
13
+ autoload :InvalidDocument, "eden_cloud_search/invalid_document"
14
+ autoload :InsufficientParametersException, "eden_cloud_search/exceptions"
15
+
16
+ def self.config
17
+ Config.instance
18
+ end
19
+
20
+ def self.configure(&block)
21
+ block.call(self.config)
22
+ end
23
+ end
24
+
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe CloudSearch::Config do
4
+ subject { CloudSearch::Config.instance }
5
+
6
+ before do
7
+ CloudSearch.configure do |config|
8
+ config.domain_id = "pl6u4t3elu7dhsbwaqbsy3y6be"
9
+ config.domain_name = "imdb-movies"
10
+ end
11
+ end
12
+
13
+ describe "after set some configurations" do
14
+ it { expect(subject.api_version).to eql("2011-02-01") }
15
+ it { expect(subject.configuration_url).to eql("https://cloudsearch.us-east-1.amazonaws.com") }
16
+ it { expect(subject.domain_id).to eql("pl6u4t3elu7dhsbwaqbsy3y6be") }
17
+ it { expect(subject.domain_name).to eql("imdb-movies") }
18
+ it { expect(subject.document_url).to eql("http://doc-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com/2011-02-01") }
19
+ it { expect(subject.region).to eql("us-east-1") }
20
+ it { expect(subject.search_url).to eql("http://search-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com/2011-02-01") }
21
+ end
22
+ end
23
+
@@ -0,0 +1,335 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe CloudSearch::Document do
6
+ it "has a 'id' attribute" do
7
+ expect(described_class.new(:id => 123).id).to eq("123")
8
+ end
9
+
10
+ it "has a 'type' attribute" do
11
+ expect(described_class.new(:type => "add").type).to eq("add")
12
+ end
13
+
14
+ it "has a 'version' attribute" do
15
+ expect(described_class.new(:version => 1234).version).to eq(1234)
16
+ end
17
+
18
+ it "has a 'lang' attribute" do
19
+ expect(described_class.new(:lang => "en").lang).to eq("en")
20
+ end
21
+
22
+ it "has a 'fields' attribute" do
23
+ expect(described_class.new(:fields => {:foo => "bar"}).fields).to eq(:foo => "bar")
24
+ end
25
+
26
+ it "clears errors between validations" do
27
+ document = described_class.new :id => nil
28
+ expect(document).to_not be_valid
29
+ document.id = "123"
30
+ document.valid?
31
+ expect(document.errors[:id]).to be_nil
32
+ end
33
+
34
+ context "id validation" do
35
+ it "is invalid without an id" do
36
+ document = described_class.new
37
+ document.valid?
38
+ expect(document.errors[:id]).to eq(["can't be blank"])
39
+ end
40
+
41
+ %w(- ? A & * ç à @ % $ ! = +).each do |char|
42
+ it "is invalid containing #{char}" do
43
+ document = described_class.new :id => "1#{char}2"
44
+ document.valid?
45
+ expect(document.errors[:id]).to eq(["is invalid"])
46
+ end
47
+ end
48
+
49
+ it "is invalid starting with an '_'" do
50
+ document = described_class.new :id => "_abc"
51
+ document.valid?
52
+ expect(document.errors[:id]).to eq(["is invalid"])
53
+ end
54
+
55
+ it "is invalid with a string containing only spaces" do
56
+ document = described_class.new :id => " "
57
+ document.valid?
58
+ expect(document.errors[:id]).to eq(["can't be blank"])
59
+ end
60
+
61
+ it "is valid with a valid id" do
62
+ document = described_class.new :id => "507c54a44a42c408f4000001"
63
+ document.valid?
64
+ expect(document.errors[:id]).to be_nil
65
+ end
66
+
67
+ it "is valid with integers" do
68
+ document = described_class.new :id => 123
69
+ document.valid?
70
+ expect(document.errors[:id]).to be_nil
71
+ end
72
+
73
+ it "converts integers to strings" do
74
+ expect(described_class.new(:id => 123).id).to eq("123")
75
+ end
76
+ end
77
+
78
+ context "version validation" do
79
+ it "is invalid with a non numeric value" do
80
+ document = described_class.new :version => "123a3545656"
81
+ document.valid?
82
+ expect(document.errors[:version]).to eq(["is invalid"])
83
+ end
84
+
85
+ it "is invalid with a nil value" do
86
+ document = described_class.new :version => nil
87
+ document.valid?
88
+ expect(document.errors[:version]).to eq(["can't be blank"])
89
+ end
90
+
91
+ it "converts strings to integers" do
92
+ expect(described_class.new(:version => "123").version).to eq(123)
93
+ end
94
+
95
+ it "does not convert strings to integers if they contain non numerical characters" do
96
+ expect(described_class.new(:version => "123abc567").version).to eq("123abc567")
97
+ end
98
+
99
+ it "is invalid if value is greater than CloudSearch::Document::MAX_VERSION" do
100
+ document = described_class.new :version => 4294967296
101
+ document.valid?
102
+ expect(document.errors[:version]).to eq(["must be less than 4294967296"])
103
+ end
104
+
105
+ it "is valid with integers greater than zero and less or equal to CloudSearch::Document::MAX_VERSION" do
106
+ document = described_class.new :version => 4294967295
107
+ document.valid?
108
+ expect(document.errors[:version]).to be_nil
109
+ end
110
+ end
111
+
112
+ context "type validation" do
113
+ it "is valid if type is 'add'" do
114
+ document = described_class.new :type => "add"
115
+ document.valid?
116
+ expect(document.errors[:type]).to be_nil
117
+ end
118
+
119
+ it "is valid if type is 'delete'" do
120
+ document = described_class.new :type => "delete"
121
+ document.valid?
122
+ expect(document.errors[:type]).to be_nil
123
+ end
124
+
125
+ it "is invalid if type is anything else" do
126
+ document = described_class.new :type => "wrong"
127
+ document.valid?
128
+ expect(document.errors[:type]).to eq(["is invalid"])
129
+ end
130
+
131
+ it "is invalid if type is nil" do
132
+ document = described_class.new :type => nil
133
+ document.valid?
134
+ expect(document.errors[:type]).to eq(["can't be blank"])
135
+ end
136
+
137
+ it "is invalid if type is a blank string" do
138
+ document = described_class.new :type => " "
139
+ document.valid?
140
+ expect(document.errors[:type]).to eq(["can't be blank"])
141
+ end
142
+ end
143
+
144
+ context "lang validation" do
145
+ context "when type is 'add'" do
146
+ it "is invalid if lang is nil" do
147
+ document = described_class.new :lang => nil, :type => "add"
148
+ document.valid?
149
+ expect(document.errors[:lang]).to eql(["can't be blank"])
150
+ end
151
+
152
+ it "is invalid if lang contains digits" do
153
+ document = described_class.new :lang => "a1", :type => "add"
154
+ document.valid?
155
+ expect(document.errors[:lang]).to eql(["is invalid"])
156
+ end
157
+
158
+ it "is invalid if lang contains more than 2 characters" do
159
+ document = described_class.new :lang => "abc", :type => "add"
160
+ document.valid?
161
+ expect(document.errors[:lang]).to eql(["is invalid"])
162
+ end
163
+
164
+ it "is invalid if lang contains upper case characters" do
165
+ document = described_class.new :lang => "Ab", :type => "add"
166
+ document.valid?
167
+ expect(document.errors[:lang]).to eql(["is invalid"])
168
+ end
169
+
170
+ it "is valid if lang contains 2 lower case characters" do
171
+ document = described_class.new :lang => "en", :type => "add"
172
+ document.valid?
173
+ expect(document.errors[:lang]).to be_nil
174
+ end
175
+ end
176
+
177
+ context "when type is 'delete'" do
178
+ it "is optional" do
179
+ document = described_class.new :type => "delete"
180
+ document.valid?
181
+ expect(document.errors[:lang]).to be_nil
182
+ end
183
+ end
184
+ end
185
+
186
+ context "fields validation" do
187
+ context "when type is 'add'" do
188
+ it "is invalid if fields is nil" do
189
+ document = described_class.new :fields => nil, :type => "add"
190
+ document.valid?
191
+ expect(document.errors[:fields]).to eql(["can't be empty"])
192
+ end
193
+
194
+ it "is invalid if fields is not a hash" do
195
+ document = described_class.new :fields => [], :type => "add"
196
+ document.valid?
197
+ expect(document.errors[:fields]).to eql(["must be an instance of Hash"])
198
+ end
199
+
200
+ it "is valid with a Hash" do
201
+ document = described_class.new :fields => {}, :type => "add"
202
+ document.valid?
203
+ expect(document.errors[:fields]).to be_nil
204
+ end
205
+ end
206
+
207
+ context "when type is 'delete'" do
208
+ it "is optional" do
209
+ document = described_class.new :type => "delete"
210
+ document.valid?
211
+ expect(document.errors[:fields]).to be_nil
212
+ end
213
+ end
214
+ end
215
+
216
+ context "#as_json" do
217
+ let(:attributes) { {
218
+ :type => type,
219
+ :id => "123abc",
220
+ :version => 123456,
221
+ :lang => "pt",
222
+ :fields => {:foo => "bar"}
223
+ } }
224
+ let(:document) { described_class.new attributes }
225
+ let(:as_json) { document.as_json }
226
+
227
+ context "when 'type' is 'add'" do
228
+ let(:type) { "add" }
229
+
230
+ it "includes the 'type' attribute" do
231
+ expect(as_json[:type]).to eq("add")
232
+ end
233
+
234
+ it "includes the 'id' attribute" do
235
+ expect(as_json[:id]).to eq("123abc")
236
+ end
237
+
238
+ it "includes the 'version' attribute" do
239
+ expect(as_json[:version]).to eq(123456)
240
+ end
241
+
242
+ it "includes the 'lang' attribute" do
243
+ expect(as_json[:lang]).to eq("pt")
244
+ end
245
+
246
+ it "includes the 'fields' attribute" do
247
+ expect(as_json[:fields]).to eq(:foo => "bar")
248
+ end
249
+ end
250
+
251
+ context "when 'type' is 'delete'" do
252
+ let(:type) { "delete" }
253
+
254
+ it "includes the 'type' attribute" do
255
+ expect(as_json[:type]).to eq("delete")
256
+ end
257
+
258
+ it "includes the 'id' attribute" do
259
+ expect(as_json[:id]).to eq("123abc")
260
+ end
261
+
262
+ it "includes the 'version' attribute" do
263
+ expect(as_json[:version]).to eq(123456)
264
+ end
265
+
266
+ it "does not include the 'lang' attribute" do
267
+ expect(as_json[:lang]).to be_nil
268
+ end
269
+
270
+ it "does not include the 'fields' attribute" do
271
+ expect(as_json[:fields]).to be_nil
272
+ end
273
+ end
274
+ end
275
+
276
+ context "#to_json" do
277
+ let(:attributes) { {
278
+ :type => type,
279
+ :id => "123abc",
280
+ :version => 123456,
281
+ :lang => "pt",
282
+ :fields => {:foo => "bar"}
283
+ } }
284
+ let(:parsed_json) { JSON.parse(described_class.new(attributes).to_json) }
285
+
286
+ context "when 'type' is 'add'" do
287
+ let(:type) { "add" }
288
+
289
+ it "includes the 'type' attribute" do
290
+ expect(parsed_json["type"]).to eq("add")
291
+ end
292
+
293
+ it "includes the 'id' attribute" do
294
+ expect(parsed_json["id"]).to eq("123abc")
295
+ end
296
+
297
+ it "includes the 'version' attribute" do
298
+ expect(parsed_json["version"]).to eq(123456)
299
+ end
300
+
301
+ it "includes the 'lang' attribute" do
302
+ expect(parsed_json["lang"]).to eq("pt")
303
+ end
304
+
305
+ it "includes the 'fields' attribute" do
306
+ expect(parsed_json["fields"]).to eq("foo" => "bar")
307
+ end
308
+ end
309
+
310
+ context "when 'type' is 'delete'" do
311
+ let(:type) { "delete" }
312
+
313
+ it "includes the 'type' attribute" do
314
+ expect(parsed_json["type"]).to eq("delete")
315
+ end
316
+
317
+ it "includes the 'id' attribute" do
318
+ expect(parsed_json["id"]).to eq("123abc")
319
+ end
320
+
321
+ it "includes the 'version' attribute" do
322
+ expect(parsed_json["version"]).to eq(123456)
323
+ end
324
+
325
+ it "does not include the 'lang' attribute" do
326
+ expect(parsed_json["lang"]).to be_nil
327
+ end
328
+
329
+ it "does not include the 'fields' attribute" do
330
+ expect(parsed_json["fields"]).to be_nil
331
+ end
332
+ end
333
+ end
334
+ end
335
+
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ describe CloudSearch::Indexer do
4
+ let(:indexer) { described_class.new }
5
+
6
+ describe "#documents" do
7
+ it "returns the list of documents to be indexed" do
8
+ doc1 = stub.as_null_object
9
+ doc2 = stub.as_null_object
10
+ indexer << doc1
11
+ indexer << doc2
12
+ expect(indexer.documents).to eq([doc1, doc2])
13
+ end
14
+
15
+ it "returns a frozen version of the documents list" do
16
+ expect {
17
+ indexer.documents << stub
18
+ }.to raise_error(RuntimeError)
19
+ end
20
+ end
21
+
22
+ describe "#<<" do
23
+ let(:document) { stub :valid? => true }
24
+
25
+ it "adds a new item to the list of documents" do
26
+ indexer << document
27
+ expect(indexer.documents).to have(1).item
28
+ end
29
+
30
+ it "is aliased to #add" do
31
+ indexer.add document
32
+ expect(indexer.documents).to have(1).item
33
+ end
34
+
35
+ it "raises an exception if the document is invalid" do
36
+ expect {
37
+ indexer << stub(:valid? => false, :errors => {})
38
+ }.to raise_error(CloudSearch::InvalidDocument)
39
+ end
40
+ end
41
+
42
+ describe "#index" do
43
+ let(:document) { CloudSearch::Document.new(
44
+ :type => type,
45
+ :id => 678,
46
+ :version => version,
47
+ :lang => :en,
48
+ :fields => {:actor => ["Cassio Marques", "Willian Fernandes"], :director => ["Lucas, George"], :title => "Troy Wars"}
49
+ ) }
50
+ let(:indexer) { described_class.new }
51
+
52
+ context "adding a new document" do
53
+ around do |example|
54
+ VCR.use_cassette "index/request/add", &example
55
+ end
56
+
57
+ let(:type) { "add" }
58
+ let(:version) { 1 }
59
+ let(:url) { "#{CloudSearch.config.document_url}/documents/batch" }
60
+ let(:json) { [document].to_json }
61
+
62
+ it "succeeds" do
63
+ indexer << document
64
+ resp, message = indexer.index
65
+ expect(resp["status"]).to eq("success")
66
+ expect(resp["adds"]).to eq(1)
67
+ expect(resp["deletes"]).to eq(0)
68
+ expect(message).to match(/^200/)
69
+ end
70
+
71
+ it "sends http headers with json format info" do
72
+ indexer << document
73
+ RestClient.should_receive(:post).with(url, kind_of(String), {"Content-Type" => "application/json", "Accept" => "application/json" }).and_return(stub(:code => 1, :length => 10, :body => '{}'))
74
+ indexer.index
75
+ end
76
+
77
+ context "when the domain id was not configured" do
78
+ around do |example|
79
+ domain_id = CloudSearch.config.domain_id
80
+ CloudSearch.config.domain_id = nil
81
+ example.call
82
+ CloudSearch.config.domain_id = domain_id
83
+ end
84
+
85
+ it "raises an error" do
86
+ expect {
87
+ indexer.index
88
+ }.to raise_error(CloudSearch::MissingConfigurationError, "Missing 'domain_id' configuration parameter")
89
+ end
90
+ end
91
+
92
+ context "when the domain name was not configured" do
93
+ around do |example|
94
+ domain_name = CloudSearch.config.domain_name
95
+ CloudSearch.config.domain_name = nil
96
+ example.call
97
+ CloudSearch.config.domain_name = domain_name
98
+ end
99
+
100
+ it "raises an error" do
101
+ expect {
102
+ indexer.index
103
+ }.to raise_error(CloudSearch::MissingConfigurationError, "Missing 'domain_name' configuration parameter")
104
+ end
105
+ end
106
+ end
107
+
108
+ context "adding a batch of documents" do
109
+ around do |example|
110
+ VCR.use_cassette "index/request/add_in_batch", &example
111
+ end
112
+
113
+ let(:type) { "add" }
114
+ let(:version) { 5 }
115
+
116
+ it "succeeds" do
117
+ indexer << document
118
+ document2 = CloudSearch::Document.new :type => type, :version => version, :id => 679, :lang => :en, :fields => {:title => "Fight Club"}
119
+ document3 = CloudSearch::Document.new :type => type, :version => version, :id => 680, :lang => :en, :fields => {:title => "Lord of the Rings"}
120
+ indexer << document2
121
+ indexer << document3
122
+ resp, message = indexer.index
123
+ expect(resp["adds"]).to eq(3)
124
+ expect(resp["status"]).to eq("success")
125
+ end
126
+ end
127
+
128
+ context "deleting a document" do
129
+ around do |example|
130
+ VCR.use_cassette "index/request/delete", &example
131
+ end
132
+
133
+ let(:type) { "delete" }
134
+ let(:version) { 2 }
135
+
136
+ it "succeeds" do
137
+ indexer << document
138
+ resp, message = indexer.index
139
+ expect(resp["status"]).to eq("success")
140
+ expect(resp["adds"]).to eq(0)
141
+ expect(resp["deletes"]).to eq(1)
142
+ expect(message).to match(/^200/)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe CloudSearch::InvalidDocument do
4
+ let(:document) { CloudSearch::Document.new }
5
+
6
+ it "has a message with the document errors" do
7
+ expect(described_class.new(document).message).to eq("id: can't be blank; version: can't be blank; type: can't be blank")
8
+ end
9
+ end