require 'httparty' require 'link_header' class OctocatHerder # This implements some additional functionality around HTTParty to # help make working with the GitHub API a little nicer. class Connection include HTTParty base_uri 'https://api.github.com' # User name to use when doing basic HTTP authentication. # # @since 0.0.1 attr_reader :user_name # Password to use when doing basic HTTP authentication. # # @since 0.0.1 attr_reader :password # The OAuth2 token to use when doing OAuth2 authentication. # # @since 0.0.1 attr_reader :oauth2_token # If provided a hash of login information, the # {OctocatHerder::Connection} will attempt to make authenticated # requests. # # @example Unauthenticated requests # connection = OctocatHerder::Connection.new # # @example Providing an OAuth2 token # connection = OctocatHerder::Connection.new :oauth2_token => 'token' # # @example Providing user name & password # connection = OctocatHerder::Connection.new :user_name => 'user', :password => 'pass' # # If no hash is provided, then unauthenticated requests will be # made. # # @since 0.0.1 # @param [Hash String>] options Login information # @option options [String] :user_name # @option options [String] :password # @option options [String] :oauth2_token def initialize(options={}) raise ArgumentError.new( "OctocatHerder::Connection does not accept: #{options.class}" ) unless options.is_a? Hash options.keys.each do |k| raise ArgumentError.new("Unknown option: '#{k}'") unless [ :user_name, :password, :oauth2_token ].include? k end if options.keys.include?(:user_name) or options.keys.include?(:password) raise ArgumentError.new("When providing :user_name or :password, both are required") unless options.keys.include?(:user_name) and options.keys.include?(:password) end if options.keys.include?(:oauth2_token) and options.keys.include?(:user_name) raise ArgumentError.new('Cannot provide both an OAuth2 token, and a user name and password') end @user_name = options[:user_name] @password = options[:password] @oauth2_token = options[:oauth2_token] if oauth2_token @httparty_options = { :headers => { 'Authorization' => "token #{oauth2_token}" } } elsif user_name @httparty_options = { :basic_auth => { :username => user_name, :password => password } } end end # Execute a GET request against the GitHub v3 API. # # @since 0.1.0 # # @param [String] end_point The part of the API URL after # +'api.github.com'+, including the leading +'/'+. # # @param [Hash] options A Hash of options to be passed down to # HTTParty, with a couple of extra options. # # @option options [true, false] :paginated Retrieve all pages from # a paginated end-point. # # @option options [Hash String>] :params # Constructed into a query string using # {OctocatHerder::Connection#query_string_from_params}. def get(end_point, options={}) paginated = options.delete(:paginated) options[:params] ||= {} options[:params][:per_page] = 100 if paginated and options[:params][:per_page].nil? result = raw_get(end_point, options) raise "Unable to retrieve #{end_point}" unless result full_result = result.parsed_response if paginated if next_page = page_from_headers(result.headers, 'next') options[:params][:page] = next_page options[:paginated] = true full_result += raw_get(end_point, options) end end full_result end # Small wrapper around HTTParty.get, which handles adding # authentication information to the API request. # # @since 0.1.0 def raw_get(end_point, options={}) options = options.dup query_params = options.delete(:params) || {} query_string = query_string_from_params(query_params) request_options = options.merge(httparty_options) if httparty_options.has_key?(:headers) and options.has_key(:headers) request_options[:headers] = options[:headers].merge(httparty_options[:headers]) end OctocatHerder::Connection.get(end_point + query_string, request_options) end # Are we making authenticated requests? # # @since 0.1.0 # @return [true, false] def authenticated_requests? if (user_name and password) or oauth2_token true else false end end # Retrieve the page number of a given 'Link:' header from a hash # of HTTP Headers # # +type+ can be one of: # ['+next+'] The immediate next page of results. # ['+last+'] The last page of first. # ['+first+'] The first page of results. # ['+prev+'] The immediate previous page of results. # # @since 0.1.0 # # @raise [ArgumentError] If type is not one of the allowed values. # # @param [Hash] headers # # @param ['next', 'last', 'first', 'prev'] type def page_from_headers(headers, type) raise ArgumentError.new( "Unknown type: #{type}" ) unless ['next', 'last', 'first', 'prev'].include? type link = LinkHeader.parse(headers['link']).find_link(['rel', type]) return unless link CGI.parse(URI.parse(link.href).query)['page'].first end # Convenience method to generate URL query strings. # # @since 0.1.0 # # @param [Hash] params A Hash of key/values to be turned into a # URL query string. Does not support nested data. # # @return [String] Empty string if params is an empty hash, # otherwise a string of the query parameters with a leading # +'?'+. def query_string_from_params(params) return '' if params.keys.empty? '?' + params.map {|k,v| "#{URI.escape("#{k}")}=#{URI.escape("#{v}")}"}.join('&') end private def httparty_options @httparty_options || {} end end end