twingly-search 5.0.1 → 5.1.0

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +33 -34
  5. data/Rakefile +0 -6
  6. data/examples/find_all_posts_mentioning_github.rb +3 -3
  7. data/examples/hello_world.rb +2 -2
  8. data/examples/livefeed_loop.rb +24 -0
  9. data/lib/twingly/livefeed/client.rb +121 -0
  10. data/lib/twingly/livefeed/error.rb +28 -0
  11. data/lib/twingly/livefeed/parser.rb +96 -0
  12. data/lib/twingly/livefeed/post.rb +66 -0
  13. data/lib/twingly/livefeed/result.rb +39 -0
  14. data/lib/twingly/livefeed/version.rb +5 -0
  15. data/lib/twingly/livefeed.rb +6 -0
  16. data/lib/twingly/search/client.rb +3 -2
  17. data/lib/twingly/search/error.rb +6 -5
  18. data/lib/twingly/search/parser.rb +39 -13
  19. data/lib/twingly/search/post.rb +65 -21
  20. data/lib/twingly/search/query.rb +46 -16
  21. data/lib/twingly/search/result.rb +11 -0
  22. data/lib/twingly/search/version.rb +1 -1
  23. data/spec/client_spec.rb +2 -2
  24. data/spec/error_spec.rb +27 -7
  25. data/spec/fixtures/incomplete_result.xml +2 -0
  26. data/spec/fixtures/livefeed/empty_api_key_result.xml +3 -0
  27. data/spec/fixtures/livefeed/non_xml_result.xml +1 -0
  28. data/spec/fixtures/livefeed/not_found_result.xml +3 -0
  29. data/spec/fixtures/livefeed/service_unavailable_result.xml +3 -0
  30. data/spec/fixtures/livefeed/unauthorized_api_key_result.xml +3 -0
  31. data/spec/fixtures/livefeed/valid_empty_result.xml +2 -0
  32. data/spec/fixtures/livefeed/valid_result.xml +79 -0
  33. data/spec/fixtures/minimal_valid_result.xml +81 -52
  34. data/spec/fixtures/nonexistent_api_key_result.xml +3 -3
  35. data/spec/fixtures/service_unavailable_result.xml +3 -3
  36. data/spec/fixtures/unauthorized_api_key_result.xml +3 -3
  37. data/spec/fixtures/undefined_error_result.xml +3 -3
  38. data/spec/fixtures/valid_empty_result.xml +2 -2
  39. data/spec/fixtures/valid_links_result.xml +36 -0
  40. data/spec/fixtures/vcr_cassettes/livefeed_valid_request.yml +169 -0
  41. data/spec/fixtures/vcr_cassettes/search_for_spotify_on_sv_blogs.yml +578 -447
  42. data/spec/fixtures/vcr_cassettes/search_without_valid_api_key.yml +15 -14
  43. data/spec/livefeed/client_spec.rb +135 -0
  44. data/spec/livefeed/error_spec.rb +51 -0
  45. data/spec/livefeed/parser_spec.rb +351 -0
  46. data/spec/livefeed/post_spec.rb +26 -0
  47. data/spec/livefeed/result_spec.rb +18 -0
  48. data/spec/parser_spec.rb +191 -94
  49. data/spec/post_spec.rb +25 -6
  50. data/spec/query_spec.rb +41 -34
  51. data/spec/result_spec.rb +1 -0
  52. data/spec/spec_helper.rb +10 -0
  53. data/twingly-search-api-ruby.gemspec +2 -3
  54. metadata +44 -24
  55. data/spec/fixtures/valid_non_blog_result.xml +0 -26
  56. data/spec/fixtures/valid_result.xml +0 -22975
@@ -2,40 +2,41 @@
2
2
  http_interactions:
3
3
  - request:
4
4
  method: get
5
- uri: https://api.twingly.com/analytics/Analytics.ashx?documentlang&key=wrong&searchpattern=something&ts&tsTo&xmloutputversion=2
5
+ uri: https://api.twingly.com/blog/search/api/v3/search?apikey=wrong&q=something
6
6
  body:
7
7
  encoding: US-ASCII
8
8
  string: ''
9
9
  headers:
10
10
  User-Agent:
11
- - Twingly Analytics Ruby Client/2.0.1
11
+ - Twingly Search Ruby Client/5.0.1
12
12
  Accept-Encoding:
13
13
  - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
14
14
  Accept:
15
15
  - "*/*"
16
16
  response:
17
17
  status:
18
- code: 200
19
- message: OK
18
+ code: 401
19
+ message: Unauthorized
20
20
  headers:
21
21
  Server:
22
22
  - nginx
23
23
  Date:
24
- - Tue, 03 Nov 2015 09:29:04 GMT
24
+ - Thu, 04 May 2017 07:14:00 GMT
25
25
  Content-Type:
26
- - text/xml; charset=utf-8
26
+ - application/xml; charset=utf-8
27
27
  Content-Length:
28
- - '183'
28
+ - '97'
29
29
  Connection:
30
30
  - keep-alive
31
31
  Cache-Control:
32
- - private
33
- Set-Cookie:
34
- - SERVERID=web01; path=/
32
+ - no-cache
33
+ Pragma:
34
+ - no-cache
35
+ Expires:
36
+ - "-1"
35
37
  body:
36
38
  encoding: UTF-8
37
- string: "<?xml version=\"1.0\" encoding=\"UTF-8\"?><blogstream xmlns=\"http://www.twingly.com\">\r\n
38
- \ <operationResult resultType=\"failure\">The API key does not exist.</operationResult>\r\n</blogstream>"
39
- http_version:
40
- recorded_at: Tue, 03 Nov 2015 09:29:04 GMT
39
+ string: <?xml version="1.0" encoding="utf-8"?><error code="40101"><message>Unauthorized</message></error>
40
+ http_version:
41
+ recorded_at: Thu, 04 May 2017 07:14:00 GMT
41
42
  recorded_with: VCR 2.9.3
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+ require "time"
3
+
4
+ module Twingly::LiveFeed
5
+ describe Client do
6
+ let(:valid_api_key) { "api_key" }
7
+ let(:max_posts) { 3 }
8
+
9
+ subject(:client) do
10
+ described_class.new do |client|
11
+ client.max_posts = max_posts
12
+ end
13
+ end
14
+
15
+ describe ".new" do
16
+ context "with API key as argument" do
17
+ subject { described_class.new(valid_api_key) }
18
+
19
+ it { should be_a Twingly::LiveFeed::Client }
20
+ end
21
+
22
+ context "without API key as argument" do
23
+ before do
24
+ expect_any_instance_of(described_class)
25
+ .to receive(:env_api_key).and_return(valid_api_key)
26
+ end
27
+
28
+ it "should be read from the environment" do
29
+ described_class.new
30
+ end
31
+ end
32
+
33
+ context "with no API key at all" do
34
+ before do
35
+ expect_any_instance_of(described_class)
36
+ .to receive(:env_api_key).and_return(nil)
37
+ end
38
+
39
+ subject { described_class.new }
40
+
41
+ it do
42
+ expect { subject }
43
+ .to raise_error(AuthError, "No API key has been provided.")
44
+ end
45
+ end
46
+
47
+ context "with block" do
48
+ it "should yield self" do
49
+ yielded_client = nil
50
+ client = described_class.new(valid_api_key) do |c|
51
+ yielded_client = c
52
+ end
53
+
54
+ expect(yielded_client).to equal(client)
55
+ end
56
+
57
+ context "when api key gets set in block" do
58
+ let(:api_key) { "api_key_from_block" }
59
+ subject do
60
+ described_class.new do |client|
61
+ client.api_key = api_key
62
+ end
63
+ end
64
+
65
+ it "should not raise an AuthError" do
66
+ expect { subject }.not_to raise_exception
67
+ end
68
+
69
+ it "should use that api key" do
70
+ expect(subject.api_key).to eq(api_key)
71
+ end
72
+ end
73
+ end
74
+
75
+ context "with optional :user_agent given" do
76
+ let(:user_agent) { "TwinglyLiveFeedTest/1.0" }
77
+ subject { described_class.new(valid_api_key, user_agent: user_agent) }
78
+
79
+ it "should use that user agent" do
80
+ expect(subject.user_agent).to eq(user_agent)
81
+ end
82
+ end
83
+ end
84
+
85
+ describe "#next_result" do
86
+ before do
87
+ client.timestamp = timestamp
88
+ end
89
+
90
+ context "with a valid timestamp" do
91
+ let(:timestamp) { Time.parse("2017-04-19T22:00:00 UTC") }
92
+
93
+ subject do
94
+ VCR.use_cassette("livefeed_valid_request") do
95
+ client.next_result
96
+ end
97
+ end
98
+
99
+ it { is_expected.to be_a(Result) }
100
+
101
+ it "should update the timestamp" do
102
+ timestamp_before = client.timestamp
103
+ subject
104
+ timestamp_after = client.timestamp
105
+
106
+ expect(timestamp_after).to be > timestamp_before
107
+ end
108
+ end
109
+
110
+ context "with an invalid timestamp" do
111
+ let(:timestamp) { "not a timestamp" }
112
+
113
+ subject { client.next_result }
114
+
115
+ it "should raise an error" do
116
+ expect{ subject }
117
+ .to raise_error(QueryError, /Not a Time object/)
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "#endpoint_url" do
123
+ subject { described_class.new(valid_api_key).endpoint_url }
124
+ let(:expected_url) do
125
+ "#{described_class::BASE_URL}#{described_class::LIVEFEED_PATH}"
126
+ end
127
+
128
+ it { is_expected.to eq(expected_url) }
129
+
130
+ it "should be parsable" do
131
+ expect(URI(subject).to_s).to eq(expected_url)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ module Twingly::LiveFeed
4
+ describe Error do
5
+ it { is_expected.to be_a(StandardError) }
6
+
7
+ let(:message) { "This is the error message!" }
8
+
9
+ describe ".from_api_response" do
10
+ subject { described_class.from_api_response(code, message) }
11
+
12
+ context "when given code 401" do
13
+ let(:code) { 401 }
14
+
15
+ it { is_expected.to be_a(AuthError) }
16
+ end
17
+
18
+ context "when given code 400" do
19
+ let(:code) { 400 }
20
+
21
+ it { is_expected.to be_a(QueryError) }
22
+ end
23
+
24
+ context "when given code 404" do
25
+ let(:code) { 404 }
26
+
27
+ it { is_expected.to be_a(QueryError) }
28
+ end
29
+
30
+ context "when given another code" do
31
+ let(:code) { 500 }
32
+
33
+ it { is_expected.to be_a(ServerError) }
34
+ end
35
+ end
36
+
37
+ describe "all error classes" do
38
+ error_classes = [
39
+ AuthError,
40
+ ServerError,
41
+ QueryError,
42
+ ]
43
+
44
+ error_classes.each do |error_class|
45
+ describe error_class do
46
+ it { is_expected.to be_kind_of(Twingly::LiveFeed::Error) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,351 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'time'
5
+
6
+ module Twingly::LiveFeed
7
+ describe Parser do
8
+ it { should respond_to(:parse) }
9
+
10
+ describe "#parse" do
11
+ let(:document) { Fixture.livefeed_get(fixture) }
12
+ subject(:result) { described_class.new.parse(document) }
13
+
14
+ context "with a valid result" do
15
+ let(:fixture) { :valid }
16
+
17
+ it { is_expected.to be_a Result }
18
+
19
+ describe "#ts" do
20
+ subject { result.ts }
21
+ it { is_expected.to eq(Time.parse("2017-04-11T15:09:48.8750635Z")) }
22
+ end
23
+
24
+ describe "#from" do
25
+ subject { result.from }
26
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:00Z")) }
27
+ end
28
+
29
+ describe "#first_post" do
30
+ subject { result.first_post }
31
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:29.267Z")) }
32
+ end
33
+
34
+ describe "#last_post" do
35
+ subject { result.last_post }
36
+ it { is_expected.to eq(Time.parse("2017-04-10T22:11:47.243Z")) }
37
+ end
38
+
39
+ describe "#number_of_posts" do
40
+ subject { result.number_of_posts }
41
+ it { is_expected.to eq(3) }
42
+ end
43
+
44
+ describe "#max_number_of_posts" do
45
+ subject { result.max_number_of_posts }
46
+ it { is_expected.to eq(3) }
47
+ end
48
+
49
+ describe "#posts" do
50
+ subject { result.posts }
51
+
52
+ it { is_expected.to all(be_a(Post)) }
53
+
54
+ describe "#count" do
55
+ subject { result.posts.count }
56
+
57
+ it { is_expected.to eq(3) }
58
+ end
59
+ end
60
+
61
+ describe "#posts.first" do
62
+ subject(:post) { result.posts.first }
63
+
64
+ describe "#id" do
65
+ subject { post.id }
66
+ it { is_expected.to eq("727444183574244541") }
67
+ end
68
+
69
+ describe "#author" do
70
+ subject { post.author }
71
+ it { is_expected.to eq("") }
72
+ end
73
+
74
+ describe "#url" do
75
+ subject { post.url }
76
+ it { is_expected.to eq("http://flinnman.blogg.se/2017/april/mandag-igen.html") }
77
+ end
78
+
79
+ describe "#title" do
80
+ subject { post.title }
81
+ it { is_expected.to eq("Måndag igen") }
82
+ end
83
+
84
+ describe "#text" do
85
+ subject { post.text }
86
+ it { is_expected.to match("Hoppla hejsan bloggy!Måndag.") }
87
+ end
88
+
89
+ describe "#language_code" do
90
+ subject { post.language_code }
91
+ it { is_expected.to eq("sv") }
92
+ end
93
+
94
+ describe "#location_code" do
95
+ subject { post.location_code }
96
+ it { is_expected.to eq("se") }
97
+ end
98
+
99
+ describe "#coordinates" do
100
+ subject { post.coordinates }
101
+ it { is_expected.to eq({}) }
102
+ end
103
+
104
+ describe "#links" do
105
+ subject { post.links }
106
+ it { is_expected.to be_empty }
107
+ end
108
+
109
+ describe "#tags" do
110
+ subject { post.tags }
111
+ it { is_expected.to eq(%w(Jag)) }
112
+ end
113
+
114
+ describe "#images" do
115
+ subject { post.images }
116
+ it { is_expected.to be_empty }
117
+ end
118
+
119
+ describe "#indexed_at" do
120
+ subject { post.indexed_at }
121
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:24Z")) }
122
+ end
123
+
124
+ describe "#published_at" do
125
+ subject { post.published_at }
126
+ it { is_expected.to eq(Time.parse("2017-04-10T19:11:11Z")) }
127
+ end
128
+
129
+ describe "#reindexed_at" do
130
+ subject { post.reindexed_at }
131
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:24Z")) }
132
+ end
133
+
134
+ describe "#inlinks_count" do
135
+ subject { post.inlinks_count }
136
+ it { is_expected.to eq(0) }
137
+ end
138
+
139
+ describe "#blog_id" do
140
+ subject { post.blog_id }
141
+ it { is_expected.to eq("10357806725947705095") }
142
+ end
143
+
144
+ describe "#blog_name" do
145
+ subject { post.blog_name }
146
+ it { is_expected.to eq("Frida L") }
147
+ end
148
+
149
+ describe "#blog_url" do
150
+ subject { post.blog_url }
151
+ it { is_expected.to eq("http://flinnman.blogg.se") }
152
+ end
153
+
154
+ describe "#blog_rank" do
155
+ subject { post.blog_rank }
156
+ it { is_expected.to eq(1) }
157
+ end
158
+
159
+ describe "#authority" do
160
+ subject { post.authority }
161
+ it { is_expected.to eq(2) }
162
+ end
163
+ end
164
+
165
+ describe "#posts.last" do
166
+ subject(:post) { result.posts.last }
167
+
168
+ describe "#id" do
169
+ subject { post.id }
170
+ it { is_expected.to eq("3062976931264108164") }
171
+ end
172
+
173
+ describe "#author" do
174
+ subject { post.author }
175
+ it { is_expected.to eq("josegacel") }
176
+ end
177
+
178
+ describe "#url" do
179
+ subject { post.url }
180
+ it { is_expected.to eq("https://josegabrielcelis.wordpress.com/2017/04/09/1476/") }
181
+ end
182
+
183
+ describe "#title" do
184
+ subject { post.title }
185
+ it { is_expected.to eq("") }
186
+ end
187
+
188
+ describe "#text" do
189
+ subject { post.text }
190
+ it { is_expected.to eq("from Instagram: http://ift.tt/2ofZdhV") }
191
+ end
192
+
193
+ describe "#language_code" do
194
+ subject { post.language_code }
195
+ it { is_expected.to eq("sv") }
196
+ end
197
+
198
+ describe "#location_code" do
199
+ subject { post.location_code }
200
+ it { is_expected.to eq("") }
201
+ end
202
+
203
+ describe "#coordinates" do
204
+ subject { post.coordinates }
205
+ it { is_expected.to eq({}) }
206
+ end
207
+
208
+ describe "#links" do
209
+ subject { post.links }
210
+ let(:expected) do
211
+ %w(
212
+ http://www.ift.tt/2ofZdhV
213
+ http://feeds.wordpress.com/1.0/gocomments/josegabrielcelis.wordpress.com/1476
214
+ )
215
+ end
216
+
217
+ it { is_expected.to eq(expected) }
218
+ end
219
+
220
+ describe "#tags" do
221
+ subject { post.tags }
222
+ it { is_expected.to eq(%w(Fotos Instagram)) }
223
+ end
224
+
225
+ describe "#images" do
226
+ subject { post.images }
227
+ it { is_expected.to be_empty }
228
+ end
229
+
230
+ describe "#indexed_at" do
231
+ subject { post.indexed_at }
232
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:28Z")) }
233
+ end
234
+
235
+ describe "#published_at" do
236
+ subject { post.published_at }
237
+ it { is_expected.to eq(Time.parse("2017-04-09T22:31:34Z")) }
238
+ end
239
+
240
+ describe "#reindexed_at" do
241
+ subject { post.reindexed_at }
242
+ it { is_expected.to eq(Time.parse("2017-04-10T22:00:28Z")) }
243
+ end
244
+
245
+ describe "#inlinks_count" do
246
+ subject { post.inlinks_count }
247
+ it { is_expected.to eq(0) }
248
+ end
249
+
250
+ describe "#blog_id" do
251
+ subject { post.blog_id }
252
+ it { is_expected.to eq("1811310581070495497") }
253
+ end
254
+
255
+ describe "#blog_name" do
256
+ subject { post.blog_name }
257
+ it { is_expected.to eq("José Gabriel Celis") }
258
+ end
259
+
260
+ describe "#blog_url" do
261
+ subject { post.blog_url }
262
+ it { is_expected.to eq("https://josegabrielcelis.wordpress.com") }
263
+ end
264
+
265
+ describe "#blog_rank" do
266
+ subject { post.blog_rank }
267
+ it { is_expected.to eq(1) }
268
+ end
269
+
270
+ describe "#authority" do
271
+ subject { post.authority }
272
+ it { is_expected.to eq(0) }
273
+ end
274
+ end
275
+ end
276
+
277
+ context "with a valid empty result" do
278
+ let(:fixture) { :valid_empty }
279
+
280
+ describe "#posts" do
281
+ subject { result.posts }
282
+ it { is_expected.to be_empty }
283
+ end
284
+
285
+ describe "#number_of_posts" do
286
+ subject { result.number_of_posts }
287
+ it { is_expected.to eq(0) }
288
+ end
289
+
290
+ describe "#first_post" do
291
+ subject { result.first_post }
292
+ it { is_expected.to eq(nil) }
293
+ end
294
+
295
+ describe "#last_post" do
296
+ subject { result.last_post }
297
+ it { is_expected.to eq(nil) }
298
+ end
299
+
300
+ describe "#next_timestamp" do
301
+ subject { result.next_timestamp }
302
+ it { is_expected.to eq(Time.parse("2017-04-25T22:00:00Z")) }
303
+ end
304
+ end
305
+
306
+ context "with an unauthorized api key result" do
307
+ let(:fixture) { :unauthorized_api_key }
308
+
309
+ it "should raise AuthError" do
310
+ expect { subject }.to raise_error(AuthError)
311
+ end
312
+ end
313
+
314
+ context "with an empty api key result" do
315
+ let(:fixture) { :empty_api_key }
316
+
317
+ it "should raise AuthError" do
318
+ expect { subject }.to raise_error(QueryError)
319
+ end
320
+ end
321
+
322
+ context "with a 404 not found result" do
323
+ let(:fixture) { :not_found }
324
+
325
+ it "should raise QueryError" do
326
+ expect { subject }.to raise_error(QueryError)
327
+ end
328
+ end
329
+
330
+ context "with a service unavailable result" do
331
+ let(:fixture) { :service_unavailable }
332
+
333
+ it "should raise ServerError" do
334
+ expect { subject }.to raise_error(ServerError)
335
+ end
336
+ end
337
+
338
+ context "with a non XML response" do
339
+ let(:fixture) { :non_xml }
340
+ let(:expected_exception_message) do
341
+ /Failed to parse response: "<html>.*/
342
+ end
343
+
344
+ it "should raise ServerError" do
345
+ expect { subject }
346
+ .to raise_error(ServerError, expected_exception_message)
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ module Twingly::LiveFeed
4
+ describe Post do
5
+ it { should respond_to :id }
6
+ it { should respond_to :author }
7
+ it { should respond_to :url }
8
+ it { should respond_to :title }
9
+ it { should respond_to :text }
10
+ it { should respond_to :location_code }
11
+ it { should respond_to :language_code }
12
+ it { should respond_to :coordinates }
13
+ it { should respond_to :links }
14
+ it { should respond_to :tags }
15
+ it { should respond_to :images }
16
+ it { should respond_to :indexed_at }
17
+ it { should respond_to :published_at }
18
+ it { should respond_to :reindexed_at }
19
+ it { should respond_to :inlinks_count }
20
+ it { should respond_to :blog_id }
21
+ it { should respond_to :blog_name }
22
+ it { should respond_to :blog_url }
23
+ it { should respond_to :blog_rank }
24
+ it { should respond_to :authority }
25
+ end
26
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Twingly::LiveFeed
4
+ describe Result do
5
+ it { should respond_to :posts }
6
+ it { should respond_to :ts }
7
+ it { should respond_to :from }
8
+ it { should respond_to :number_of_posts }
9
+ it { should respond_to :max_number_of_posts }
10
+ it { should respond_to :first_post }
11
+ it { should respond_to :last_post }
12
+ it { should respond_to :next_timestamp }
13
+
14
+ context "before query has populated responses" do
15
+ its(:posts) { should be_empty }
16
+ end
17
+ end
18
+ end