mirror of
https://github.com/samsonjs/instapaper.git
synced 2026-03-25 08:55:49 +00:00
switch from faraday to http.rb
This commit is contained in:
parent
169a5f0dd9
commit
b501a59a3d
13 changed files with 152 additions and 190 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
42
lib/instapaper/http/headers.rb
Normal file
42
lib/instapaper/http/headers.rb
Normal 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
|
||||
86
lib/instapaper/http/request.rb
Normal file
86
lib/instapaper/http/request.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue