lib/tracker_api/client.rb in tracker_api-0.1.0 vs lib/tracker_api/client.rb in tracker_api-0.2.0
- old
+ new
@@ -1,33 +1,36 @@
module TrackerApi
class Client
- USER_AGENT = "Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}) TrackerApi/#{TrackerApi::VERSION} Faraday/#{Faraday::VERSION}".freeze
+ USER_AGENT = "Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}) TrackerApi/#{TrackerApi::VERSION} Faraday/#{Faraday::VERSION}".freeze
- attr_accessor :url, :api_version, :token, :logger, :connection
+ # Header keys that can be passed in options hash to {#get},{#paginate}
+ CONVENIENCE_HEADERS = Set.new([:accept, :content_type])
+ attr_reader :url, :api_version, :token, :logger, :connection, :auto_paginate, :last_response
+
# Create Pivotal Tracker API client.
#
# @param [Hash] options the connection options
# @option options [String] :token API token to use for requests
# @option options [String] :url Main HTTP API root
+ # @option options [Boolean] :auto_paginate Client should perform pagination automatically. Default true.
# @option options [String] :api_version The API version URL path
# @option options [String] :logger Custom logger
# @option options [String] :adapter Custom http adapter to configure Faraday with
# @option options [String] :connection_options Connection options to pass to Faraday
#
# @example Creating a Client
# Client.new token: 'my-super-special-token'
def initialize(options={})
- url = options[:url] || 'https://www.pivotaltracker.com'
- @url = URI.parse(url).to_s
-
- @api_version = options[:api_version] || '/services/v5'
- @logger = options[:logger] || Logger.new(nil)
- adapter = options[:adapter] || :net_http
- connection_options = options[:connection_options] || { ssl: { verify: true } }
+ url = options.fetch(:url, 'https://www.pivotaltracker.com')
+ @url = Addressable::URI.parse(url).to_s
+ @api_version = options.fetch(:api_version, '/services/v5')
+ @logger = options.fetch(:logger, Logger.new(nil))
+ adapter = options.fetch(:adapter, :net_http)
+ connection_options = options.fetch(:connection_options, { ssl: { verify: true } })
+ @auto_paginate = options.fetch(:auto_paginate, true)
@token = options[:token]
-
raise 'Missing required options: :token' unless @token
@connection = Faraday.new({ url: @url }.merge(connection_options)) do |builder|
# response
builder.use Faraday::Response::RaiseError
@@ -40,32 +43,142 @@
builder.use TrackerApi::Logger, @logger
builder.adapter adapter
end
end
- def request(options={})
- method = options[:method] || :get
- url = options[:url] || File.join(@url, @api_version, options[:path])
- token = options[:token] || @token
+ # Make a HTTP GET request
+ #
+ # @param path [String] The path, relative to api endpoint
+ # @param options [Hash] Query and header params for request
+ # @return [Faraday::Response]
+ def get(path, options = {})
+ request(:get, parse_query_and_convenience_headers(path, options))
+ end
+
+ # Make one or more HTTP GET requests, optionally fetching
+ # the next page of results from information passed back in headers
+ # based on value in {#auto_paginate}.
+ #
+ # @param path [String] The path, relative to {#api_endpoint}
+ # @param options [Hash] Query and header params for request
+ # @param block [Block] Block to perform the data concatenation of the
+ # multiple requests. The block is called with two parameters, the first
+ # contains the contents of the requests so far and the second parameter
+ # contains the latest response.
+ # @return [Array]
+ def paginate(path, options = {}, &block)
+ opts = parse_query_and_convenience_headers path, options.dup
+ @last_response = request :get, opts
+ data = @last_response.body
+ raise TrackerApi::Errors::UnexpectedData, 'Array expected' unless data.is_a? Array
+
+ if @auto_paginate
+ pager = Pagination.new @last_response.headers
+
+ while pager.more?
+ opts[:params].update(pager.next_page_params)
+
+ @last_response = request :get, opts
+ pager = Pagination.new @last_response.headers
+ if block_given?
+ yield(data, @last_response)
+ else
+ data.concat(@last_response.body) if @last_response.body.is_a?(Array)
+ end
+ end
+ end
+
+ data
+ end
+
+ # Get projects
+ #
+ # @param [Hash] params
+ # @return [Array[TrackerApi::Resources::Project]]
+ def projects(params={})
+ Endpoints::Projects.new(self).get(params)
+ end
+
+ # Get project
+ #
+ # @param [Hash] params
+ # @return [TrackerApi::Resources::Project]
+ def project(id, params={})
+ Endpoints::Project.new(self).get(id, params)
+ end
+
+ private
+
+ def parse_query_and_convenience_headers(path, options)
+ raise 'Path can not be blank.' if path.to_s.empty?
+
+ opts = { body: options[:body] }
+
+ opts[:url] = options[:url] || File.join(@url, @api_version, path.to_s)
+ opts[:method] = options[:method] || :get
+ opts[:params] = options[:params] || {}
+ opts[:token] = options[:token] || @token
+ headers = { 'User-Agent' => USER_AGENT,
+ 'X-TrackerToken' => opts.fetch(:token) }.merge(options.fetch(:headers, {}))
+
+ CONVENIENCE_HEADERS.each do |h|
+ if header = options[h]
+ headers[h] = header
+ end
+ end
+ opts[:headers] = headers
+
+ opts
+ end
+
+ def request(method, options = {})
+ url = options.fetch(:url)
params = options[:params] || {}
body = options[:body]
- headers = { 'User-Agent' => USER_AGENT, 'X-TrackerToken' => token }.merge(options[:headers] || {})
+ headers = options[:headers]
- connection.send(method) do |req|
- req.url url
+ @last_response = response = connection.send(method) do |req|
+ req.url(url)
req.headers.merge!(headers)
req.params.merge!(params)
req.body = body
end
+ response
rescue Faraday::Error::ClientError => e
raise TrackerApi::Error.new(e)
end
- def projects(params={})
- Endpoints::Projects.new(self).get(params)
- end
+ class Pagination
+ attr_accessor :headers, :total, :limit, :offset, :returned
- def project(id, params={})
- Endpoints::Project.new(self).get(id, params)
+ def initialize(headers)
+ @headers = headers
+ @total = headers['x-tracker-pagination-total'].to_i
+ @limit = headers['x-tracker-pagination-limit'].to_i
+ @offset = headers['x-tracker-pagination-offset'].to_i
+ @returned = headers['x-tracker-pagination-returned'].to_i
+
+ # if offset is negative (e.g. Iterations Endpoint).
+ # For the 'Done' scope, negative numbers can be passed, which
+ # specifies the number of iterations preceding the 'Current' iteration.
+ # then need to adjust the negative offset to account for a smaller total,
+ # and set total to zero since we are paginating from -X to 0.
+ if @offset < 0
+ @offset = -@total if @offset.abs > @total
+ @total = 0
+ end
+ end
+
+ def more?
+ (offset + limit) < total
+ end
+
+ def next_offset
+ offset + limit
+ end
+
+ def next_page_params
+ { limit: limit, offset: next_offset }
+ end
end
end
end