switch from faraday to http.rb

This commit is contained in:
stve 2015-02-14 00:23:56 -05:00
parent 169a5f0dd9
commit b501a59a3d
13 changed files with 152 additions and 190 deletions

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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