twingly-search 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4a32c03b5a124011f9486d4724a9390f74fd88f2
4
- data.tar.gz: b7c26d1ba53842a58e40f25ae54abbe9c0dee8f7
3
+ metadata.gz: dd9953d1e03a6e3b8876f3a4b86580ddcfeeeccf
4
+ data.tar.gz: 36f8ccb71ca25e1016d00e366652e06dc71b6321
5
5
  SHA512:
6
- metadata.gz: 5a3ffe5d6257cb17d9e32df29cac7e9014829b99d71aef1c0adef5cae4d338018dcbc378af4d5b4d299753851fac6c4b10c24e38ebea8e23a950647d8c250134
7
- data.tar.gz: 9142bf21df1a9655f89c4df65ab3072a2bf7e3efc054cec70cb332ea7c37a83fb29d7442b77d40707e8843512d2c51fe5dfdde5b3741c8675aff8aa7fee258da
6
+ metadata.gz: 525c3009c34290b6a86a6a397c9e907d9defbf7089e82e0786274d0c46146d79d4bf9d69cb673120ebd50ea1f10626a13b94d5e113590095f1c557edbc6380ef
7
+ data.tar.gz: 3df9d3ddd711c7185005a81c93ce3fb4b362a7a526698a2203e475c2e6234054e054269ebb2d45aedb44bc4c31ea88d083598952debd78af2c63f38dcc680edd
data/.gitignore CHANGED
@@ -2,3 +2,5 @@
2
2
  Gemfile.lock
3
3
  .ruby-version
4
4
  *.gem
5
+ .yardoc
6
+ doc/
data/.travis.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - 1.9.3
5
- - 2.0.0
6
- - 2.1
7
- - 2.2
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - 2.1
7
+ - 2.2
8
+ - jruby-9.0.0.0
8
9
  deploy:
9
10
  provider: rubygems
10
11
  api_key:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Change Log
2
2
 
3
+ ## [v4.0.0](https://github.com/twingly/twingly-search-api-ruby/tree/v4.0.0)
4
+
5
+ [Full Changelog](https://github.com/twingly/twingly-search-api-ruby/compare/v3.0.0...v4.0.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Add JRuby 9000 to supported Ruby versions [\#27](https://github.com/twingly/twingly-search-api-ruby/issues/27)
10
+ - Rename analytics to search [\#21](https://github.com/twingly/twingly-search-api-ruby/issues/21)
11
+
12
+ **Fixed bugs:**
13
+
14
+ - Handle non-XML responses from server [\#31](https://github.com/twingly/twingly-search-api-ruby/issues/31)
15
+ - Changelog is not generated correctly [\#25](https://github.com/twingly/twingly-search-api-ruby/issues/25)
16
+
17
+ **Merged pull requests:**
18
+
19
+ - Example script: Retry on server error [\#35](https://github.com/twingly/twingly-search-api-ruby/pull/35) ([jage](https://github.com/jage))
20
+ - Yield self [\#34](https://github.com/twingly/twingly-search-api-ruby/pull/34) ([roback](https://github.com/roback))
21
+ - Handle non-XML responses from server [\#33](https://github.com/twingly/twingly-search-api-ruby/pull/33) ([roback](https://github.com/roback))
22
+ - Add YARD documentation comments [\#32](https://github.com/twingly/twingly-search-api-ruby/pull/32) ([roback](https://github.com/roback))
23
+ - Test with JRuby 9000 on travis [\#30](https://github.com/twingly/twingly-search-api-ruby/pull/30) ([roback](https://github.com/roback))
24
+ - Handle exceptions [\#29](https://github.com/twingly/twingly-search-api-ruby/pull/29) ([roback](https://github.com/roback))
25
+ - Let client handle api call [\#28](https://github.com/twingly/twingly-search-api-ruby/pull/28) ([roback](https://github.com/roback))
26
+ - Cleanup [\#26](https://github.com/twingly/twingly-search-api-ruby/pull/26) ([roback](https://github.com/roback))
27
+ - Show deprecation warnings for twingly-analytics [\#23](https://github.com/twingly/twingly-search-api-ruby/pull/23) ([roback](https://github.com/roback))
28
+
29
+ ## [v3.0.0](https://github.com/twingly/twingly-search-api-ruby/tree/v3.0.0) (2015-11-20)
30
+ [Full Changelog](https://github.com/twingly/twingly-search-api-ruby/compare/2.0.1...v3.0.0)
31
+
32
+ **Implemented enhancements:**
33
+
34
+ - Improve "Development and release" section in README [\#19](https://github.com/twingly/twingly-search-api-ruby/issues/19)
35
+
36
+ **Merged pull requests:**
37
+
38
+ - Rename analytics to search [\#24](https://github.com/twingly/twingly-search-api-ruby/pull/24) ([roback](https://github.com/roback))
39
+ - Improve "Development and release" section in README [\#22](https://github.com/twingly/twingly-search-api-ruby/pull/22) ([roback](https://github.com/roback))
40
+ - Rename analytics to search in readme [\#20](https://github.com/twingly/twingly-search-api-ruby/pull/20) ([roback](https://github.com/roback))
41
+
3
42
  ## [2.0.1](https://github.com/twingly/twingly-search-api-ruby/tree/2.0.1) (2015-09-24)
4
43
  [Full Changelog](https://github.com/twingly/twingly-search-api-ruby/compare/2.0.0...2.0.1)
5
44
 
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Twingly Search API Ruby
2
2
 
3
- [![Build Status](https://travis-ci.org/twingly/twingly-analytics-api-ruby.png?branch=master)](https://travis-ci.org/twingly/twingly-analytics-api-ruby)
4
- [![Code Climate](https://codeclimate.com/github/twingly/twingly-analytics-api-ruby.png)](https://codeclimate.com/github/twingly/twingly-analytics-api-ruby)
3
+ [![Build Status](https://travis-ci.org/twingly/twingly-search-api-ruby.png?branch=master)](https://travis-ci.org/twingly/twingly-search-api-ruby)
4
+ [![Code Climate](https://codeclimate.com/github/twingly/twingly-search-api-ruby/badges/gpa.svg)](https://codeclimate.com/github/twingly/twingly-search-api-ruby)
5
5
 
6
6
  A Ruby gem for Twingly's Search API (previously known as Analytics API). Twingly is a blog search service that provides a searchable API known as [Twingly Search API](https://developer.twingly.com/resources/search/).
7
7
 
@@ -9,25 +9,30 @@ A Ruby gem for Twingly's Search API (previously known as Analytics API). Twingly
9
9
 
10
10
  Install via RubyGems
11
11
 
12
- ```Shell
12
+ ```shell
13
13
  gem install twingly-search
14
14
  ```
15
15
 
16
16
  Or add to your application's [Gemfile](http://bundler.io/gemfile.html) and then run `bundle`
17
17
 
18
- ```Ruby
19
- gem 'twingly-search'
18
+ ```ruby
19
+ gem "twingly-search"
20
20
  ```
21
21
 
22
22
  ## Usage
23
23
 
24
- ```Ruby
25
- require 'twingly/search'
24
+ ```ruby
25
+ require "twingly/search"
26
+
27
+ client = Twingly::Search::Client.new do |client|
28
+ client.user_agent = "MyCompany/1.0"
29
+ end
30
+
31
+ query = client.query do |query|
32
+ query.pattern = "github page-size:10"
33
+ query.language = "sv"
34
+ end
26
35
 
27
- client = Twingly::Search::Client.new
28
- query = client.query
29
- query.pattern = 'github page-size:10'
30
- query.language = 'sv'
31
36
  result = query.execute
32
37
  => #<Twingly::Search::Result:0x3ff7adcbe3d4 @posts, @number_of_matches_returned=10, @number_of_matches_total=3035221>
33
38
  result.posts # will include all returned posts
@@ -35,14 +40,19 @@ result.posts # will include all returned posts
35
40
 
36
41
  The `twingly-search` gem talks to a commercial blog search API and requires an API key. Best practice is to set the `TWINGLY_SEARCH_KEY` environment variable to the obtained key. `Twingly::Search::Client` can be passed a key at initialization if your setup does not allow environment variables.
37
42
 
38
- Example code can be found in [examples/](examples/).
43
+ To learn more about the features of this gem, read the [gem documentation] or check out the example code that can be found in [examples/](examples/).
39
44
 
40
- Too learn more about the capabilities of this API you should read the [Twingly Search API documentation](https://developer.twingly.com/resources/search/).
45
+ To learn more about the capabilities of the API, please read the [Twingly Search API documentation].
46
+
47
+ [gem documentation]: http://www.rubydoc.info/github/twingly/twingly-search-api-ruby
48
+ [Twingly Search API documentation]: https://developer.twingly.com/resources/search/
41
49
 
42
50
  ## Requirements
43
51
 
44
- * API key, contact sales@twingly.com to get one
45
- * Ruby 1.9, 2.0, 2.1, 2.2
52
+ * API key, contact sales@twingly.com via [twingly.com](http://www.twingly.com/try-for-free/) to get one
53
+ * Ruby
54
+ * Ruby 1.9, 2.0, 2.1, 2.2
55
+ * JRuby 9000
46
56
 
47
57
  ## Development and release
48
58
 
@@ -53,6 +63,16 @@ Too learn more about the capabilities of this API you should read the [Twingly S
53
63
 
54
64
  [releases page]: https://github.com/twingly/twingly-search-api-ruby/releases
55
65
 
66
+ ### Documentation
67
+
68
+ This gem is documented using [YARD]. To start a local YARD server run:
69
+
70
+ bundle exec rake yard:server
71
+
72
+ The YARD server reloads the documentation automatically so there is no need to restart it when making changes.
73
+
74
+ [YARD]: http://yardoc.org/
75
+
56
76
  ## License
57
77
 
58
78
  The MIT License (MIT)
data/Rakefile CHANGED
@@ -16,8 +16,26 @@ GitHubChangelogGenerator::RakeTask.new(:changelog) do |config|
16
16
  config.project = "twingly-search-api-ruby"
17
17
  end
18
18
 
19
+ namespace :yard do
20
+ require "yard"
21
+ require "yard/rake/yardoc_task"
22
+
23
+ desc "Generate Yardoc documentation"
24
+ YARD::Rake::YardocTask.new(:generate)
25
+
26
+ desc "Start a Yard server"
27
+ task :server do
28
+ sh("yard", "server", "--reload")
29
+ end
30
+ end
31
+
32
+ desc "Synonym for yard:generate"
33
+ task yard: "yard:generate"
34
+
19
35
  desc "Synonym for spec"
20
- task :test => :spec
36
+ task test: :spec
37
+
21
38
  desc "Synonym for spec"
22
- task :tests => :spec
23
- task :default => :spec
39
+ task tests: :spec
40
+
41
+ task default: :spec
data/examples/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'twingly-search', path: '../'
4
+ gem "retryable"
@@ -3,15 +3,22 @@ Bundler.require
3
3
  class SearchPostStream
4
4
  def initialize(keyword, language: nil)
5
5
  # Set environment variable TWINGLY_SEARCH_KEY
6
- client = Twingly::Search::Client.new
7
- @query = client.query
8
- @query.language = language
9
- @query.pattern = "sort-order:asc sort:published #{keyword}"
6
+ client = Twingly::Search::Client.new do |client|
7
+ client.user_agent = "MyCompany/1.0" # Set optional user agent
8
+ end
9
+
10
+ @query = client.query do |query|
11
+ query.language = language
12
+ query.pattern = "sort-order:asc sort:published #{keyword}"
13
+ end
10
14
  end
11
15
 
16
+ # Run block for each blog post returned from api.
17
+ # Uses a sliding time-based window to get all results.
18
+ # @see https://developer.twingly.com/resources/search/#pagination
12
19
  def each
13
20
  loop do
14
- result = @query.execute
21
+ result = execute_with_retry
15
22
  result.posts.each do |post|
16
23
  yield post
17
24
  end
@@ -21,6 +28,14 @@ class SearchPostStream
21
28
  @query.start_time = result.posts.last.published
22
29
  end
23
30
  end
31
+
32
+ private
33
+
34
+ def execute_with_retry
35
+ Retryable.retryable(on: Twingly::Search::ServerError) do
36
+ @query.execute
37
+ end
38
+ end
24
39
  end
25
40
 
26
41
  stream = SearchPostStream.new("(github) AND (hipchat OR slack)")
@@ -2,9 +2,11 @@ Bundler.require
2
2
 
3
3
  # Set environment variable TWINGLY_SEARCH_KEY
4
4
  client = Twingly::Search::Client.new
5
- query = client.query
6
- query.pattern = '"hello world"'
7
- query.start_time = Time.now - (24 * 3600) # search last day
5
+ query = client.query do |query|
6
+ query.pattern = '"hello world"'
7
+ query.start_time = Time.now - (24 * 3600) # search last day
8
+ end
9
+
8
10
  result = query.execute
9
11
  result.posts.each do |post|
10
12
  puts post.url
@@ -1,6 +1,7 @@
1
+ require 'twingly/search/version'
2
+ require 'twingly/search/error'
1
3
  require 'twingly/search/client'
2
4
  require 'twingly/search/query'
3
5
  require 'twingly/search/result'
4
6
  require 'twingly/search/parser'
5
7
  require 'twingly/search/post'
6
- require 'twingly/search/version'
@@ -1,19 +1,76 @@
1
+ require "faraday"
2
+
1
3
  module Twingly
2
4
  module Search
5
+ # Twingly Search API client
3
6
  class Client
4
- attr_accessor :api_key
7
+ attr_accessor :api_key, :user_agent
8
+
9
+ BASE_URL = "https://api.twingly.com"
10
+ SEARCH_PATH = "/analytics/Analytics.ashx"
11
+
12
+ DEFAULT_USER_AGENT = "Twingly Search Ruby Client/#{VERSION}"
13
+
14
+ # Creates a new Twingly Search API client
15
+ #
16
+ # @param api_key [optional, String] the API key provided by Twingly.
17
+ # If nil, reads api_key from environment (TWINGLY_SEARCH_KEY).
18
+ # @param options [Hash]
19
+ # @option options [String] :user_agent the user agent to be used
20
+ # for all requests
21
+ # @raise [AuthError] if an API key is not set.
22
+ def initialize(api_key = nil, options = {})
23
+ @api_key = api_key
24
+ @user_agent = options.fetch(:user_agent) { DEFAULT_USER_AGENT }
5
25
 
6
- def initialize(api_key = nil)
7
- @api_key = api_key || env_api_key || fail("Missing API key")
26
+ yield self if block_given?
27
+
28
+ @api_key ||= env_api_key || api_key_missing
29
+ end
30
+
31
+ # Returns a new Query object connected to this client
32
+ #
33
+ # @yield [Query]
34
+ # @return [Query]
35
+ def query(&block)
36
+ Query.new(self, &block)
8
37
  end
9
38
 
10
- def query
11
- Query.new(self)
39
+ # Executes the given Query and returns the result
40
+ #
41
+ # This method should not be called manually, as that is
42
+ # handled by {Query#execute}.
43
+ #
44
+ # @param query [Query] the query to be executed.
45
+ # @return [Result]
46
+ def execute_query(query)
47
+ response_body = get_response(query).body
48
+ Parser.new.parse(response_body)
12
49
  end
13
- private
50
+
51
+ # @return [String] the API endpoint URL
52
+ def endpoint_url
53
+ "#{BASE_URL}#{SEARCH_PATH}"
54
+ end
55
+
56
+ private
57
+
14
58
  def env_api_key
15
59
  ENV['TWINGLY_SEARCH_KEY']
16
60
  end
61
+
62
+ def get_response(query)
63
+ connection = Faraday.new(url: BASE_URL) do |faraday|
64
+ faraday.request :url_encoded
65
+ faraday.adapter Faraday.default_adapter
66
+ end
67
+ connection.headers[:user_agent] = user_agent
68
+ connection.get(SEARCH_PATH, query.request_parameters)
69
+ end
70
+
71
+ def api_key_missing
72
+ fail AuthError, "No API key has been provided."
73
+ end
17
74
  end
18
75
  end
19
76
  end
@@ -0,0 +1,27 @@
1
+ module Twingly
2
+ module Search
3
+ class Error < StandardError
4
+ # @param [String] message API response error message.
5
+ # @return [Error] an instance of {AuthError} or {ServerError}.
6
+ def self.from_api_response_message(message)
7
+ error =
8
+ if message =~ /API key/
9
+ AuthError
10
+ else
11
+ ServerError
12
+ end
13
+
14
+ error.new(message)
15
+ end
16
+ end
17
+
18
+ class AuthError < Error
19
+ end
20
+
21
+ class ServerError < Error
22
+ end
23
+
24
+ class QueryError < Error
25
+ end
26
+ end
27
+ end
@@ -3,29 +3,43 @@ require 'nokogiri'
3
3
  module Twingly
4
4
  module Search
5
5
  class Parser
6
+ # Parse an API response body.
7
+ #
8
+ # @param [String] document containing an API response XML.
9
+ # @raise [Error] which error depends on the API response (see {Error.from_api_response_message}).
10
+ # @return [Result] containing the result.
6
11
  def parse(document)
7
- result = Result.new
8
12
  nokogiri = Nokogiri::XML(document)
9
13
 
10
- failure = nokogiri.at_xpath('//name:blogstream/name:operationResult[@resultType="failure"]', :name => 'http://www.twingly.com')
11
- fail failure.text if failure
14
+ failure = nokogiri.at_xpath('//name:blogstream/name:operationResult[@resultType="failure"]', name: 'http://www.twingly.com')
15
+ handle_failure(failure) if failure
16
+
17
+ data_node = nokogiri.at_xpath('/twinglydata')
18
+ handle_non_xml_document(nokogiri) unless data_node
19
+
20
+ create_result(data_node)
21
+ end
22
+
23
+ private
12
24
 
13
- result.number_of_matches_returned = nokogiri.at_xpath('/twinglydata/@numberOfMatchesReturned').value.to_i
14
- result.number_of_matches_total = nokogiri.at_xpath('/twinglydata/@numberOfMatchesTotal').value.to_i
15
- result.seconds_elapsed = nokogiri.at_xpath('/twinglydata/@secondsElapsed').value.to_f
25
+ def create_result(data_node)
26
+ result = Result.new
27
+ result.number_of_matches_returned = data_node.attribute('numberOfMatchesReturned').value.to_i
28
+ result.number_of_matches_total = data_node.attribute('numberOfMatchesTotal').value.to_i
29
+ result.seconds_elapsed = data_node.attribute('secondsElapsed').value.to_f
16
30
 
17
- nokogiri.xpath('//post').each do |post|
31
+ data_node.xpath('//post').each do |post|
18
32
  result.posts << parse_post(post)
19
33
  end
20
34
 
21
35
  result
22
36
  end
23
- private
37
+
24
38
  def parse_post(element)
25
39
  post_params = {}
26
40
  element.element_children.each do |child|
27
41
  if child.name == 'tags'
28
- post_params[child.name] = parse_tags(child) if child.name == 'tags'
42
+ post_params[child.name] = parse_tags(child)
29
43
  else
30
44
  post_params[child.name] = child.text
31
45
  end
@@ -40,6 +54,15 @@ module Twingly
40
54
  child.text
41
55
  end
42
56
  end
57
+
58
+ def handle_failure(failure)
59
+ fail Error.from_api_response_message(failure.text)
60
+ end
61
+
62
+ def handle_non_xml_document(document)
63
+ response_text = document.search('//text()').map(&:text)
64
+ fail ServerError, response_text
65
+ end
43
66
  end
44
67
  end
45
68
  end
@@ -4,10 +4,29 @@ require 'date'
4
4
 
5
5
  module Twingly
6
6
  module Search
7
+ # A blog post
8
+ #
9
+ # @attr_reader [String] url the post URL.
10
+ # @attr_reader [String] title the post title.
11
+ # @attr_reader [String] summary the blog post text.
12
+ # @attr_reader [String] language_code ISO two letter language code for the
13
+ # language that the post was written in.
14
+ # @attr_reader [DateTime] indexed the time, in UTC, when this post was indexed by Twingly.
15
+ # @attr_reader [DateTime] published the time, in UTC, when this post was published.
16
+ # @attr_reader [String] blog_url the blog URL.
17
+ # @attr_reader [String] blog_name name of the blog.
18
+ # @attr_reader [String] authority the blog's authority/influence.
19
+ # See https://developer.twingly.com/resources/search/#authority
20
+ # @attr_reader [Integer] blog_rank the rank of the blog, based on authority and language.
21
+ # See https://developer.twingly.com/resources/search/#authority
22
+ # @attr_reader [Array] tags
7
23
  class Post
8
24
  attr_reader :url, :title, :summary, :language_code, :indexed,
9
25
  :published, :blog_url, :blog_name, :authority, :blog_rank, :tags
10
26
 
27
+ # Sets all instance variables for the {Post}, given a Hash.
28
+ #
29
+ # @param [Hash] params containing blog post data.
11
30
  def set_values(params)
12
31
  @url = params.fetch('url')
13
32
  @title = params.fetch('title')
@@ -1,43 +1,62 @@
1
- require 'faraday'
1
+ require "faraday"
2
2
 
3
3
  module Twingly
4
4
  module Search
5
+ # Twingly Search API query
6
+ #
7
+ # @attr [String] pattern the search query.
8
+ # @attr [String] language which language to restrict the query to.
9
+ # @attr [Client] client the client that this query is connected to.
10
+ # @attr [Time, #to_time] start_time search for posts published after
11
+ # this time (inclusive).
12
+ # @attr [Time, #to_time] end_time search for posts published before
13
+ # this time (inclusive).
5
14
  class Query
6
15
  attr_accessor :pattern, :language, :client, :start_time, :end_time
7
16
 
8
- BASE_URL = 'https://api.twingly.com'
9
- SEARCH_PATH = '/analytics/Analytics.ashx'
10
-
17
+ # No need to call this method manually, instead use {Client#query}.
18
+ #
19
+ # @param client [Client] the client that this query should be connected to.
11
20
  def initialize(client)
12
21
  @client = client
22
+ yield self if block_given?
13
23
  end
14
24
 
25
+ # @return [String] the request url for the query.
15
26
  def url
16
- "#{BASE_URL}#{SEARCH_PATH}?#{url_parameters}"
27
+ "#{client.endpoint_url}?#{url_parameters}"
17
28
  end
18
29
 
30
+ # Executes the query and returns the result.
31
+ #
32
+ # @raise [QueryError] if {#pattern} is empty.
33
+ # @return [Result] the result for this query.
19
34
  def execute
20
- Parser.new.parse(get_response.body)
35
+ @client.execute_query(self)
21
36
  end
22
37
 
38
+ # @see #url
39
+ # @return [String] the query part of the request url.
23
40
  def url_parameters
24
41
  Faraday::Utils.build_query(request_parameters)
25
42
  end
26
43
 
44
+ # @raise [QueryError] if {#pattern} is empty.
45
+ # @return [Hash] the request parameters.
27
46
  def request_parameters
28
- fail("Missing pattern") if pattern.to_s.empty?
47
+ fail QueryError, "Missing pattern" if pattern.to_s.empty?
29
48
 
30
49
  {
31
- :key => client.api_key,
32
- :searchpattern => pattern,
33
- :documentlang => language,
34
- :ts => ts,
35
- :tsTo => ts_to,
36
- :xmloutputversion => 2
50
+ key: client.api_key,
51
+ searchpattern: pattern,
52
+ documentlang: language,
53
+ ts: ts,
54
+ tsTo: ts_to,
55
+ xmloutputversion: 2,
37
56
  }
38
57
  end
39
58
 
40
- private
59
+ private
41
60
 
42
61
  def ts
43
62
  start_time.to_time.strftime("%F %T") if start_time
@@ -46,15 +65,6 @@ module Twingly
46
65
  def ts_to
47
66
  end_time.to_time.strftime("%F %T") if end_time
48
67
  end
49
-
50
- def get_response
51
- connection = Faraday.new(:url => BASE_URL) do |faraday|
52
- faraday.request :url_encoded
53
- faraday.adapter Faraday.default_adapter
54
- end
55
- connection.headers[:user_agent] = "Twingly Search Ruby Client/#{VERSION}"
56
- connection.get(SEARCH_PATH, request_parameters)
57
- end
58
68
  end
59
69
  end
60
70
  end
@@ -1,13 +1,25 @@
1
1
  module Twingly
2
2
  module Search
3
+ # Represents a result from a {Query} to the Search API
4
+ #
5
+ # @see Query#execute
6
+ # @attr [Integer] number_of_matches_returned number of {Post}s
7
+ # the {Query} returned.
8
+ # @attr [Integer] number_of_matches_total total number of {Post}s
9
+ # the {Query} matched.
10
+ # @attr [Integer] seconds_elapsed number of seconds it took to
11
+ # execute the {Query}.
3
12
  class Result
4
13
  attr_accessor :number_of_matches_returned, :number_of_matches_total,
5
14
  :seconds_elapsed
6
15
 
16
+ # @return [Array<Post>] all posts that matched the {Query}.
7
17
  def posts
8
18
  @posts ||= []
9
19
  end
10
20
 
21
+ # @return [true] if this result includes all {Post}s that matched the {Query}.
22
+ # @return [false] if there are more {Post}s to fetch from the API.
11
23
  def all_results_returned?
12
24
  number_of_matches_returned.to_i == number_of_matches_total.to_i
13
25
  end
@@ -1,5 +1,5 @@
1
1
  module Twingly
2
2
  module Search
3
- VERSION = "3.0.0"
3
+ VERSION = "4.0.0"
4
4
  end
5
5
  end
data/spec/client_spec.rb CHANGED
@@ -4,24 +4,111 @@ include Twingly::Search
4
4
 
5
5
  describe Client do
6
6
  subject { Client.new('api_key') }
7
- context 'with API key as arguments' do
8
- it { should be_a Client }
9
- end
10
7
 
11
- context 'with API key from ENV variable' do
12
- before { allow_any_instance_of(Client).to receive(:env_api_key).and_return('api_key') }
13
- subject { Client.new }
14
- it { should be_a Client }
15
- end
8
+ describe ".new" do
9
+ context 'with API key as arguments' do
10
+ it { should be_a Client }
11
+ end
12
+
13
+ it "BASE_URL should be parsable" do
14
+ expect(URI(Client::BASE_URL).to_s).to eq(Client::BASE_URL)
15
+ end
16
+
17
+ context 'with API key from ENV variable' do
18
+ before { allow_any_instance_of(Client).to receive(:env_api_key).and_return('api_key') }
19
+ subject { Client.new }
20
+ it { should be_a Client }
21
+ end
22
+
23
+ context 'without valid API key' do
24
+ before { allow_any_instance_of(Client).to receive(:env_api_key).and_return(nil) }
25
+ subject { Client.new }
26
+ it { expect { subject }.to raise_error(AuthError, "No API key has been provided.") }
27
+ end
28
+
29
+ context "with optional :user_agent given" do
30
+ let(:user_agent) { "TwinglySearchTest/1.0" }
31
+ subject { Client.new('api_key', user_agent: user_agent) }
32
+
33
+ it "should use that user agent" do
34
+ expect(subject.user_agent).to eq(user_agent)
35
+ end
36
+ end
37
+
38
+ context "with block" do
39
+ it "should yield self" do
40
+ yielded_client = nil
41
+ client = Client.new("api_key") do |c|
42
+ yielded_client = c
43
+ end
44
+
45
+ expect(yielded_client).to equal(client)
46
+ end
47
+
48
+ context "when api key gets set in block" do
49
+ before { allow_any_instance_of(Client).to receive(:env_api_key).and_return(nil) }
50
+ let(:api_key) { "api_key_from_block" }
51
+ subject do
52
+ Client.new do |client|
53
+ client.api_key = api_key
54
+ end
55
+ end
56
+
57
+ it "should not raise an AuthError" do
58
+ expect { subject }.not_to raise_exception
59
+ end
16
60
 
17
- context 'without valid API key' do
18
- before { allow_any_instance_of(Client).to receive(:env_api_key).and_return(nil) }
19
- subject { Client.new }
20
- it { expect { subject }.to raise_error(RuntimeError, 'Missing API key') }
61
+ it "should use that api key" do
62
+ expect(subject.api_key).to eq(api_key)
63
+ end
64
+ end
65
+ end
21
66
  end
22
67
 
23
68
  describe '#query' do
24
69
  subject { Client.new('api_key').query }
25
70
  it { should be_a Query }
71
+
72
+ context "with block" do
73
+ subject { Client.new("api_key") }
74
+
75
+ it "should yield the query" do
76
+ yielded_query = nil
77
+ query = subject.query do |q|
78
+ yielded_query = q
79
+ end
80
+
81
+ expect(yielded_query).to equal(query)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "#execute_query" do
87
+ context "with invalid API key" do
88
+ subject { Client.new("wrong") }
89
+
90
+ let(:query) do
91
+ query = subject.query
92
+ query.pattern = "something"
93
+ query
94
+ end
95
+
96
+ it "should raise error on invalid API key" do
97
+ VCR.use_cassette("search_without_valid_api_key") do
98
+ expect { subject.execute_query(query) }.to raise_error(AuthError, "The API key does not exist.")
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ describe "#endpoint_url" do
105
+ subject { Client.new("api_key").endpoint_url }
106
+ let(:expected) { "#{Client::BASE_URL}#{Client::SEARCH_PATH}" }
107
+
108
+ it { is_expected.to eq(expected) }
109
+
110
+ it "should be parsable" do
111
+ expect(URI(subject).to_s).to eq(expected)
112
+ end
26
113
  end
27
114
  end
@@ -0,0 +1,35 @@
1
+ require "spec_helper"
2
+
3
+ describe Twingly::Search::Error do
4
+ it { is_expected.to be_a(StandardError) }
5
+
6
+ describe ".from_api_response_message" do
7
+ subject { described_class.from_api_response_message(server_response_message) }
8
+
9
+ context "when given message containing 'API key'" do
10
+ let(:server_response_message) { "... API key ..." }
11
+
12
+ it { is_expected.to be_an(AuthError) }
13
+ end
14
+
15
+ context "when given a server error message" do
16
+ let(:server_response_message) { "An error occured." }
17
+
18
+ it { is_expected.to be_an(ServerError) }
19
+ end
20
+ end
21
+
22
+ describe "all error classes" do
23
+ error_classes = [
24
+ AuthError,
25
+ ServerError,
26
+ QueryError,
27
+ ]
28
+
29
+ error_classes.each do |error_class|
30
+ describe error_class do
31
+ it { is_expected.to be_kind_of(Twingly::Search::Error) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ <html><body><h1>503 Service Unavailable</h1>
2
+ No server is available to handle this request.
3
+ </body></html>
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><blogstream xmlns="http://www.twingly.com">
2
+ <operationResult resultType="failure">Authentication service unavailable.</operationResult>
3
+ </blogstream>
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><blogstream xmlns="http://www.twingly.com">
2
+ <operationResult resultType="failure">The API key does not grant access to the Search API.</operationResult>
3
+ </blogstream>
@@ -0,0 +1,3 @@
1
+ <?xml version="1.0" encoding="UTF-8"?><blogstream xmlns="http://www.twingly.com">
2
+ <operationResult resultType="failure">Something went wrong.</operationResult>
3
+ </blogstream>
data/spec/parser_spec.rb CHANGED
@@ -5,10 +5,53 @@ include Twingly::Search
5
5
  describe Parser do
6
6
  it { should respond_to(:parse) }
7
7
 
8
- let(:document) { File.read('spec/fixtures/valid_result.xml') }
9
-
10
8
  describe "#parse" do
11
9
  subject { Parser.new.parse(document) }
12
- it { should be_a Result }
10
+
11
+ context "with a valid result" do
12
+ let(:document) { Fixture.get(:valid) }
13
+
14
+ it { is_expected.to be_a Result }
15
+ end
16
+
17
+ context "with a nonexistent api key result" do
18
+ let(:document) { Fixture.get(:nonexistent_api_key) }
19
+
20
+ it "should raise AuthError" do
21
+ expect { subject }.to raise_error(AuthError)
22
+ end
23
+ end
24
+
25
+ context "with an unauthorized api key result" do
26
+ let(:document) { Fixture.get(:unauthorized_api_key) }
27
+
28
+ it "should raise AuthError" do
29
+ expect { subject }.to raise_error(AuthError)
30
+ end
31
+ end
32
+
33
+ context "with a service unavailable result" do
34
+ let(:document) { Fixture.get(:service_unavailable) }
35
+
36
+ it "should raise ServerError" do
37
+ expect { subject }.to raise_error(ServerError)
38
+ end
39
+ end
40
+
41
+ context "with a undefined error result" do
42
+ let(:document) { Fixture.get(:undefined_error) }
43
+
44
+ it "should raise ServerError" do
45
+ expect { subject }.to raise_error(ServerError)
46
+ end
47
+ end
48
+
49
+ context "with a undefined error result" do
50
+ let(:document) { Fixture.get(:non_xml) }
51
+
52
+ it "should raise ServerError" do
53
+ expect { subject }.to raise_error(ServerError, /503 Service Unavailable/)
54
+ end
55
+ end
13
56
  end
14
57
  end
data/spec/query_spec.rb CHANGED
@@ -4,25 +4,13 @@ require 'vcr_setup'
4
4
  include Twingly::Search
5
5
 
6
6
  describe Query do
7
-
8
- it "BASE_URL should be parsable" do
9
- expect(URI(Query::BASE_URL).to_s).to eq(Query::BASE_URL)
10
- end
11
-
12
- context "without client" do
13
- subject { Query.new }
14
-
15
- it "should not work" do
16
- expect { subject }.to raise_error(ArgumentError)
17
- end
18
- end
7
+ let(:client_double) { double("Client") }
19
8
 
20
9
  before(:each) do
21
- @client = double('client')
22
- allow(@client).to receive(:api_key).and_return('api_key')
10
+ allow(client_double).to receive(:api_key).and_return("api_key")
23
11
  end
24
12
 
25
- subject { Query.new(@client) }
13
+ subject { Query.new(client_double) }
26
14
 
27
15
  it { should respond_to(:pattern) }
28
16
  it { should respond_to(:language) }
@@ -31,8 +19,34 @@ describe Query do
31
19
  it { should respond_to(:execute) }
32
20
  it { should respond_to(:client) }
33
21
 
22
+ describe ".new" do
23
+ context "without client" do
24
+ subject { Query.new }
25
+
26
+ it "should not work" do
27
+ expect { subject }.to raise_error(ArgumentError)
28
+ end
29
+ end
30
+
31
+ context "with block" do
32
+ it "should yield self" do
33
+ yielded_query = nil
34
+ query = Query.new(client_double) do |q|
35
+ yielded_query = q
36
+ end
37
+
38
+ expect(yielded_query).to equal(query)
39
+ end
40
+ end
41
+ end
42
+
34
43
  describe "#url" do
35
- let(:query) { Query.new(@client) }
44
+ before do
45
+ endpoint_url = "https://api.twingly.com/analytics/Analytics.ashx"
46
+ allow(client_double).to receive(:endpoint_url).and_return(endpoint_url)
47
+ end
48
+
49
+ let(:query) { Query.new(client_double) }
36
50
 
37
51
  context "with valid pattern" do
38
52
  before { query.pattern = "christmas" }
@@ -43,7 +57,7 @@ describe Query do
43
57
 
44
58
  context "without valid pattern" do
45
59
  it "raises an error" do
46
- expect { query.url }.to raise_error(RuntimeError, "Missing pattern")
60
+ expect { query.url }.to raise_error(QueryError, "Missing pattern")
47
61
  end
48
62
  end
49
63
 
@@ -51,7 +65,7 @@ describe Query do
51
65
  before { query.pattern = "" }
52
66
 
53
67
  it "raises an error" do
54
- expect { query.url }.to raise_error(RuntimeError, "Missing pattern")
68
+ expect { query.url }.to raise_error(QueryError, "Missing pattern")
55
69
  end
56
70
  end
57
71
  end
@@ -89,17 +103,18 @@ describe Query do
89
103
  end
90
104
 
91
105
  describe "#execute" do
92
- context "with invalid API key" do
93
- subject {
94
- query = Query.new(Client.new('wrong'))
106
+ context "when called" do
107
+ let(:client) { instance_double("Client", "api_key") }
108
+ subject do
109
+ query = Query.new(client)
95
110
  query.pattern = 'something'
96
111
  query
97
- }
112
+ end
98
113
 
99
- it "should raise error on invalid API key" do
100
- VCR.use_cassette('search_without_valid_api_key') do
101
- expect { subject.execute }.to raise_error(RuntimeError, "The API key does not exist.")
102
- end
114
+ it "should send the query to the client" do
115
+ expect(client).to receive(:execute_query).with(subject)
116
+
117
+ subject.execute
103
118
  end
104
119
  end
105
120
 
data/spec/spec_helper.rb CHANGED
@@ -1 +1,8 @@
1
1
  require File.dirname(__FILE__) + '/../lib/twingly/search'
2
+
3
+ class Fixture
4
+ def self.get(fixture_name)
5
+ filename = "spec/fixtures/#{fixture_name}_result.xml"
6
+ File.read(filename)
7
+ end
8
+ end
@@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency "webmock", "~> 1.0"
28
28
  spec.add_development_dependency "rake", "~> 0"
29
29
  spec.add_development_dependency "github_changelog_generator", "~> 1.8"
30
+ spec.add_development_dependency "yard", "~> 0.8"
30
31
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twingly-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Twingly AB
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-20 00:00:00.000000000 Z
11
+ date: 2015-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -128,6 +128,20 @@ dependencies:
128
128
  - - "~>"
129
129
  - !ruby/object:Gem::Version
130
130
  version: '1.8'
131
+ - !ruby/object:Gem::Dependency
132
+ name: yard
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.8'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.8'
131
145
  description: Twingly Search is a product from Twingly AB
132
146
  email:
133
147
  - support@twingly.com
@@ -147,13 +161,19 @@ files:
147
161
  - examples/hello_world.rb
148
162
  - lib/twingly/search.rb
149
163
  - lib/twingly/search/client.rb
164
+ - lib/twingly/search/error.rb
150
165
  - lib/twingly/search/parser.rb
151
166
  - lib/twingly/search/post.rb
152
167
  - lib/twingly/search/query.rb
153
168
  - lib/twingly/search/result.rb
154
169
  - lib/twingly/search/version.rb
155
170
  - spec/client_spec.rb
156
- - spec/fixtures/invalid_result.xml
171
+ - spec/error_spec.rb
172
+ - spec/fixtures/non_xml_result.xml
173
+ - spec/fixtures/nonexistent_api_key_result.xml
174
+ - spec/fixtures/service_unavailable_result.xml
175
+ - spec/fixtures/unauthorized_api_key_result.xml
176
+ - spec/fixtures/undefined_error_result.xml
157
177
  - spec/fixtures/valid_result.xml
158
178
  - spec/fixtures/vcr_cassettes/search_for_spotify_on_sv_blogs.yml
159
179
  - spec/fixtures/vcr_cassettes/search_without_valid_api_key.yml
@@ -184,13 +204,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
204
  version: '0'
185
205
  requirements: []
186
206
  rubyforge_project:
187
- rubygems_version: 2.5.0
207
+ rubygems_version: 2.4.5.1
188
208
  signing_key:
189
209
  specification_version: 4
190
210
  summary: Ruby API client for Twingly Search
191
211
  test_files:
192
212
  - spec/client_spec.rb
193
- - spec/fixtures/invalid_result.xml
213
+ - spec/error_spec.rb
214
+ - spec/fixtures/non_xml_result.xml
215
+ - spec/fixtures/nonexistent_api_key_result.xml
216
+ - spec/fixtures/service_unavailable_result.xml
217
+ - spec/fixtures/unauthorized_api_key_result.xml
218
+ - spec/fixtures/undefined_error_result.xml
194
219
  - spec/fixtures/valid_result.xml
195
220
  - spec/fixtures/vcr_cassettes/search_for_spotify_on_sv_blogs.yml
196
221
  - spec/fixtures/vcr_cassettes/search_without_valid_api_key.yml
@@ -200,3 +225,4 @@ test_files:
200
225
  - spec/result_spec.rb
201
226
  - spec/spec_helper.rb
202
227
  - spec/vcr_setup.rb
228
+ has_rdoc: