From b501a59a3def9d7a53b13ecfaf04c0048d3c7618 Mon Sep 17 00:00:00 2001 From: stve Date: Sat, 14 Feb 2015 00:23:56 -0500 Subject: [PATCH] switch from faraday to http.rb --- instapaper.gemspec | 3 +- lib/faraday/response/parse_json.rb | 31 --------- lib/faraday/response/raise_http_1xxx.rb | 64 ------------------ lib/instapaper/api.rb | 2 - lib/instapaper/api/bookmarks.rb | 6 +- lib/instapaper/api/folders.rb | 2 +- lib/instapaper/api/highlights.rb | 2 +- lib/instapaper/api/oauth.rb | 4 +- lib/instapaper/client.rb | 67 ++----------------- lib/instapaper/http/headers.rb | 42 ++++++++++++ lib/instapaper/http/request.rb | 86 +++++++++++++++++++++++++ lib/instapaper/{api => http}/utils.rb | 11 +++- spec/faraday/response_spec.rb | 22 ------- 13 files changed, 152 insertions(+), 190 deletions(-) delete mode 100644 lib/faraday/response/parse_json.rb delete mode 100644 lib/faraday/response/raise_http_1xxx.rb create mode 100644 lib/instapaper/http/headers.rb create mode 100644 lib/instapaper/http/request.rb rename lib/instapaper/{api => http}/utils.rb (81%) delete mode 100644 spec/faraday/response_spec.rb diff --git a/instapaper.gemspec b/instapaper.gemspec index 2b4da64..0bb4f74 100644 --- a/instapaper.gemspec +++ b/instapaper.gemspec @@ -3,7 +3,8 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'instapaper/version' Gem::Specification.new do |spec| - spec.add_dependency 'faraday_middleware', '~> 0.7' + spec.add_dependency 'addressable', '~> 2.3' + spec.add_dependency 'http', '~> 0.7.1' spec.add_dependency 'multi_json', '~> 1' spec.add_dependency 'simple_oauth' spec.add_dependency 'values' diff --git a/lib/faraday/response/parse_json.rb b/lib/faraday/response/parse_json.rb deleted file mode 100644 index d6e61e4..0000000 --- a/lib/faraday/response/parse_json.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'faraday' -require 'json' - -module Instapaper - module API - module Response - class ParseJson < Faraday::Response::Middleware - WHITESPACE_REGEX = /\A^\s*$\z/ - - def parse(body) - case body - when WHITESPACE_REGEX, nil - nil - else - JSON.parse(body, symbolize_names: true) - end - end - - def on_complete(response) - response.body = parse(response.body) if respond_to?(:parse) && !unparsable_status_codes.include?(response.status) - end - - def unparsable_status_codes - [204, 301, 302, 304] - end - end - end - end -end - -Faraday::Response.register_middleware instapaper_parse_json: Instapaper::API::Response::ParseJson diff --git a/lib/faraday/response/raise_http_1xxx.rb b/lib/faraday/response/raise_http_1xxx.rb deleted file mode 100644 index d98a0e6..0000000 --- a/lib/faraday/response/raise_http_1xxx.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'faraday' - -# @private -module Faraday - # @private - class Response::RaiseHttp1xxx < Response::Middleware # rubocop:disable Style/ClassAndModuleChildren - def on_complete(env) # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength - case env[:status].to_i - - # general errors - - when 1040 - fail Instapaper::Error, error_message(env, 'Rate-limit exceeded.') - when 1041 - fail Instapaper::Error, error_message(env, 'Subscription account required.') - when 1042 - fail Instapaper::Error, error_message(env, 'Application is suspended.') - - # bookmark errors - - when 1220 - fail Instapaper::Error, error_message(env, 'Domain requires full content to be supplied.') - when 1221 - fail Instapaper::Error, error_message(env, 'Domain has opted out of Instapaper compatibility.') - when 1240 - fail Instapaper::Error, error_message(env, 'Invalid URL specified.') - when 1241 - fail Instapaper::Error, error_message(env, 'Invalid or missing bookmark_id.') - when 1242 - fail Instapaper::Error, error_message(env, 'Invalid or missing folder_id.') - when 1243 - fail Instapaper::Error, error_message(env, 'Invalid or missing progress.') - when 1244 - fail Instapaper::Error, error_message(env, 'Invalid or missing progress_timestamp.') - when 1245 - fail Instapaper::Error, error_message(env, 'Private bookmarks require supplied content.') - when 1250 - fail Instapaper::Error, error_message(env, 'Unexpected error when saving bookmark.') - - # folder errors - - when 1250 - fail Instapaper::Error, error_message(env, 'Invalid or missing title.') - when 1251 - fail Instapaper::Error, error_message(env, 'User already has a folder with this title.') - when 1252 - fail Instapaper::Error, error_message(env, 'Cannot add bookmarks to this folder.') - - # operational errors - - when 1500 - fail Instapaper::Error, error_message(env, 'Unexpected service error.') - when 1550 - fail Instapaper::Error, error_message(env, 'Error generating text version of this URL.') - end - end - - private - - def error_message(env, body = nil) - "#{env[:method].to_s.upcase} #{env[:url]}: #{[env[:status].to_s + ':', body].compact.join(' ')}." - end - end -end diff --git a/lib/instapaper/api.rb b/lib/instapaper/api.rb index d655c5d..972303a 100644 --- a/lib/instapaper/api.rb +++ b/lib/instapaper/api.rb @@ -3,7 +3,6 @@ require 'instapaper/api/bookmarks' require 'instapaper/api/folders' require 'instapaper/api/highlights' require 'instapaper/api/oauth' -require 'instapaper/api/utils' module Instapaper module API @@ -12,6 +11,5 @@ module Instapaper include Instapaper::API::Folders include Instapaper::API::Highlights include Instapaper::API::OAuth - include Instapaper::API::Utils end end diff --git a/lib/instapaper/api/bookmarks.rb b/lib/instapaper/api/bookmarks.rb index 88f833b..88b1720 100644 --- a/lib/instapaper/api/bookmarks.rb +++ b/lib/instapaper/api/bookmarks.rb @@ -67,10 +67,10 @@ module Instapaper # Returns the specified bookmark's processed text-view HTML, which is # always text/html encoded as UTF-8. # @param bookmark_id [String] The id of the bookmark. - def text(bookmark_id) - post('/api/1/bookmarks/get_text', {bookmark_id: bookmark_id}, true).body + def get_text(bookmark_id) + perform_post_with_unparsed_response('/api/1/bookmarks/get_text', {bookmark_id: bookmark_id}) end - alias_method :get_text, :text + alias_method :text, :get_text end end end diff --git a/lib/instapaper/api/folders.rb b/lib/instapaper/api/folders.rb index 11f3f31..9488d70 100644 --- a/lib/instapaper/api/folders.rb +++ b/lib/instapaper/api/folders.rb @@ -19,7 +19,7 @@ module Instapaper # Deletes the folder and moves any articles in it to the Archive. # @param folder_id [String] The id of the folder. def delete_folder(folder_id) - perform_post_with_empty_response('/api/1/folders/delete', folder_id: folder_id) + perform_post_with_unparsed_response('/api/1/folders/delete', folder_id: folder_id) true end diff --git a/lib/instapaper/api/highlights.rb b/lib/instapaper/api/highlights.rb index 74df005..7784c40 100644 --- a/lib/instapaper/api/highlights.rb +++ b/lib/instapaper/api/highlights.rb @@ -24,7 +24,7 @@ module Instapaper # @param highlight_id [String, Integer] # @return [Boolean] def delete_highlight(highlight_id, options = {}) - perform_post_with_empty_response("/api/1.1/highlights/#{highlight_id}/delete", options) + perform_post_with_unparsed_response("/api/1.1/highlights/#{highlight_id}/delete", options) true end end diff --git a/lib/instapaper/api/oauth.rb b/lib/instapaper/api/oauth.rb index fea9d34..b72243b 100644 --- a/lib/instapaper/api/oauth.rb +++ b/lib/instapaper/api/oauth.rb @@ -4,8 +4,8 @@ module Instapaper module OAuth # Gets an OAuth access token for a user. def access_token(username, password) - response = post('/api/1/oauth/access_token', {x_auth_username: username, x_auth_password: password, x_auth_mode: 'client_auth'}, true) - params = response.body.split('&') + response = perform_post_with_unparsed_response('/api/1/oauth/access_token', {x_auth_username: username, x_auth_password: password, x_auth_mode: 'client_auth'}) + params = response.split('&') values = params.map { |part| part.split('=') }.flatten values.unshift('error') if values.length == 1 Hash[*values] diff --git a/lib/instapaper/client.rb b/lib/instapaper/client.rb index c89ad0d..0bda158 100644 --- a/lib/instapaper/client.rb +++ b/lib/instapaper/client.rb @@ -1,18 +1,15 @@ require 'instapaper/api' -require 'instapaper/error' +require 'instapaper/http/utils' require 'instapaper/version' -require 'faraday_middleware' -require 'faraday/response/parse_json' -require 'faraday/response/raise_http_1xxx' module Instapaper # Wrapper for the Instapaper REST API class Client include Instapaper::API + include Instapaper::HTTP::Utils # An array of valid keys in the options hash when configuring a {Instapaper::API} VALID_OPTIONS_KEYS = [ - :adapter, :consumer_key, :consumer_secret, :endpoint, @@ -22,11 +19,6 @@ module Instapaper :user_agent, :connection_options].freeze - # The adapter that will be used to connect if none is set - # - # @note The default faraday adapter is Net::HTTP. - DEFAULT_ADAPTER = :net_http - # By default, don't set an application key DEFAULT_CONSUMER_KEY = nil @@ -51,7 +43,6 @@ module Instapaper DEFAULT_CONNECTION_OPTIONS = {} # @private - attr_accessor :adapter attr_accessor :consumer_key attr_accessor :consumer_secret attr_accessor :endpoint @@ -69,60 +60,15 @@ module Instapaper end end - private - - def connection(raw = false) # rubocop:disable AbcSize, CyclomaticComplexity, MethodLength, PerceivedComplexity - merged_options = connection_defaults.merge(@connection_options) - - Faraday.new(merged_options) do |builder| - if authenticated? - builder.use Faraday::Request::OAuth, authentication - else - builder.use Faraday::Request::OAuth, consumer_tokens - end - builder.use Faraday::Request::Multipart - builder.use Faraday::Request::UrlEncoded - builder.use Instapaper::API::Response::ParseJson unless raw - builder.use Faraday::Response::RaiseHttp1xxx - builder.adapter(adapter) - end - end - - def connection_defaults - { - headers: { - 'Accept' => 'application/json', - 'User-Agent' => @user_agent, - }, - proxy: @proxy, - url: @endpoint, - } - end - - # Perform an HTTP POST request - def post(path, options = {}, raw = false) - request(:post, path, options, raw) - end - - # Perform an HTTP request - def request(method, path, options, raw = false) - response = connection(raw).send(method) do |request| - request.path = path - request.body = options unless options.empty? - end - raw ? response : response.body - end - alias_method :perform_request, :request - # Authentication hash # # @return [Hash] - def authentication + def credentials { consumer_key: @consumer_key, consumer_secret: @consumer_secret, - token: @oauth_token, - token_secret: @oauth_token_secret, + oauth_token: @oauth_token, + oauth_token_secret: @oauth_token_secret, } end @@ -140,9 +86,10 @@ module Instapaper authentication.values.all? end + private + # Reset all configuration options to defaults def reset # rubocop:disable MethodLength - @adapter = DEFAULT_ADAPTER @consumer_key = DEFAULT_CONSUMER_KEY @consumer_secret = DEFAULT_CONSUMER_SECRET @endpoint = DEFAULT_ENDPOINT diff --git a/lib/instapaper/http/headers.rb b/lib/instapaper/http/headers.rb new file mode 100644 index 0000000..1260568 --- /dev/null +++ b/lib/instapaper/http/headers.rb @@ -0,0 +1,42 @@ +require 'addressable/uri' +require 'base64' +require 'simple_oauth' + +module Instapaper + module HTTP + class Headers + def initialize(client, request_method, url, options = {}) + @client = client + @request_method = request_method.to_sym + @uri = Addressable::URI.parse(url) + @options = options + end + + def auth_header + SimpleOAuth::Header.new(@request_method, @uri, @options, credentials.merge(ignore_extra_keys: true)) + end + + def request_headers + { + user_agent: @client.user_agent, + authorization: auth_header, + } + end + + private + + # Authentication hash + # + # @return [Hash] + def credentials + { + consumer_key: @client.consumer_key, + consumer_secret: @client.consumer_secret, + token: @client.oauth_token, + token_secret: @client.oauth_token_secret, + } + end + + end + end +end diff --git a/lib/instapaper/http/request.rb b/lib/instapaper/http/request.rb new file mode 100644 index 0000000..e140187 --- /dev/null +++ b/lib/instapaper/http/request.rb @@ -0,0 +1,86 @@ +require 'addressable/uri' +require 'http' +require 'json' +require 'net/https' +require 'openssl' +require 'instapaper/error' +require 'instapaper/http/headers' + +module Instapaper + module HTTP + class Request + BASE_URL = 'https://www.instapaper.com' + attr_accessor :client, :headers, :multipart, :options, :path, + :rate_limit, :request_method, :uri + alias_method :verb, :request_method + + # @param client [Instapaper::Client] + # @param request_method [String, Symbol] + # @param path [String] + # @param options [Hash] + # @return [Instapaper::HTTP::Request] + def initialize(client, request_method, path, options = {}) + @client = client + @request_method = request_method + @uri = Addressable::URI.parse(path.start_with?('http') ? path : BASE_URL + path) + @path = uri.path + @options = options + end + + # @return [Array, Hash] + def perform + perform_request + end + + private + + def perform_request + raw = @options.delete(:raw) + @headers = Instapaper::HTTP::Headers.new(@client, @request_method, @uri, @options).request_headers + options_key = @request_method == :get ? :params : :form + response = ::HTTP.with(@headers).public_send(@request_method, @uri.to_s, options_key => @options) + response_body = raw ? response.to_s : symbolize_keys!(response.parse) + response_headers = response.headers + fail_or_return_response_body(response.code, response_body, response_headers) + end + + def fail_or_return_response_body(code, body, headers) + error = nil # error(code, body, headers) + fail(error) if error + body + end + + def error(code, body, headers) + klass = Instapaper::Error::ERRORS[code] + if klass == Instapaper::Error::Forbidden + forbidden_error(body, headers) + elsif !klass.nil? + klass.from_response(body, headers) + end + end + + def forbidden_error(body, headers) + error = Instapaper::Error::Forbidden.from_response(body, headers) + klass = Instapaper::Error::FORBIDDEN_MESSAGES[error.message] + if klass + klass.from_response(body, headers) + else + error + end + end + + def symbolize_keys!(object) + if object.is_a?(Array) + object.each_with_index do |val, index| + object[index] = symbolize_keys!(val) + end + elsif object.is_a?(Hash) + object.keys.each do |key| + object[key.to_sym] = symbolize_keys!(object.delete(key)) + end + end + object + end + end + end +end diff --git a/lib/instapaper/api/utils.rb b/lib/instapaper/http/utils.rb similarity index 81% rename from lib/instapaper/api/utils.rb rename to lib/instapaper/http/utils.rb index f7482c0..90cb68f 100644 --- a/lib/instapaper/api/utils.rb +++ b/lib/instapaper/http/utils.rb @@ -1,7 +1,8 @@ +require 'instapaper/http/request' require 'instapaper/object' module Instapaper - module API + module HTTP module Utils private @@ -41,8 +42,12 @@ module Instapaper # @param path [String] # @param options [Hash] - def perform_post_with_empty_response(path, options) - perform_request(:post, path, options, true) + def perform_post_with_unparsed_response(path, options) + perform_request(:post, path, options.merge(raw: true)) + end + + def perform_request(method, path, options) + Instapaper::HTTP::Request.new(self, method, path, options).perform end end end diff --git a/spec/faraday/response_spec.rb b/spec/faraday/response_spec.rb deleted file mode 100644 index 39d91f5..0000000 --- a/spec/faraday/response_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe Faraday::Response do - before do - @client = Instapaper::Client.new - end - - [1040, 1041, 1042, 1220, 1221, 1240, 1241, 1242, 1243, 1244, 1245, 1250, - 1251, 1252, 1500, 1550].each do |status| - context "when HTTP status is #{status}" do - before do - stub_post('/api/1/folders/list').to_return(status: status) - end - - it 'should raise Instapaper::Error error' do - expect do - @client.folders - end.to raise_error(Instapaper::Error) - end - end - end -end