footrest 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/footrest.gemspec +1 -2
- data/lib/footrest/connection.rb +4 -3
- data/lib/footrest/follow_redirects.rb +142 -0
- data/lib/footrest/parse_json.rb +47 -0
- data/lib/footrest/response_middleware.rb +78 -0
- data/lib/footrest/version.rb +1 -1
- metadata +5 -18
data/footrest.gemspec
CHANGED
@@ -26,8 +26,7 @@ Gem::Specification.new do |gem|
|
|
26
26
|
gem.add_development_dependency "debugger"
|
27
27
|
gem.add_development_dependency "pry"
|
28
28
|
|
29
|
-
gem.add_dependency "faraday", "~> 0.
|
30
|
-
gem.add_dependency "faraday_middleware", "~> 0.9.0"
|
29
|
+
gem.add_dependency "faraday", "~> 0.9.0"
|
31
30
|
gem.add_dependency "activesupport", ">= 3.0.0"
|
32
31
|
|
33
32
|
# Parses Link headers formatted according to RFC 5988 draft spec
|
data/lib/footrest/connection.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
require 'faraday'
|
2
|
-
require 'faraday_middleware'
|
3
2
|
require 'footrest/http_error'
|
4
3
|
require 'footrest/pagination'
|
4
|
+
require 'footrest/follow_redirects'
|
5
|
+
require 'footrest/parse_json'
|
5
6
|
|
6
7
|
module Footrest
|
7
8
|
module Connection
|
@@ -14,8 +15,8 @@ module Footrest
|
|
14
15
|
faraday.request :url_encoded
|
15
16
|
faraday.response :logger if config[:logging]
|
16
17
|
faraday.adapter Faraday.default_adapter
|
17
|
-
faraday.use
|
18
|
-
faraday.use
|
18
|
+
faraday.use Footrest::FollowRedirects
|
19
|
+
faraday.use Footrest::ParseJson, :content_type => /\bjson$/
|
19
20
|
faraday.use Footrest::RaiseFootrestErrors
|
20
21
|
faraday.use Footrest::Pagination
|
21
22
|
faraday.headers[:accept] = "application/json"
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Footrest
|
5
|
+
# Public: Exception thrown when the maximum amount of requests is exceeded.
|
6
|
+
class RedirectLimitReached < Faraday::Error::ClientError
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
def initialize(response)
|
10
|
+
super "too many redirects; last one to: #{response['location']}"
|
11
|
+
@response = response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Public: Follow HTTP 301, 302, 303, and 307 redirects for GET, PATCH, POST,
|
16
|
+
# PUT, and DELETE requests.
|
17
|
+
#
|
18
|
+
# This middleware does not follow the HTTP specification for HTTP 302, by
|
19
|
+
# default, in that it follows the improper implementation used by most major
|
20
|
+
# web browsers which forces the redirected request to become a GET request
|
21
|
+
# regardless of the original request method.
|
22
|
+
#
|
23
|
+
# For HTTP 301, 302, and 303, the original request is transformed into a
|
24
|
+
# GET request to the response Location, by default. However, with standards
|
25
|
+
# compliance enabled, a 302 will instead act in accordance with the HTTP
|
26
|
+
# specification, which will replay the original request to the received
|
27
|
+
# Location, just as with a 307.
|
28
|
+
#
|
29
|
+
# For HTTP 307, the original request is replayed to the response Location,
|
30
|
+
# including original HTTP request method (GET, POST, PUT, DELETE, PATCH),
|
31
|
+
# original headers, and original body.
|
32
|
+
#
|
33
|
+
# This middleware currently only works with synchronous requests; in other
|
34
|
+
# words, it doesn't support parallelism.
|
35
|
+
class FollowRedirects < Faraday::Middleware
|
36
|
+
# HTTP methods for which 30x redirects can be followed
|
37
|
+
ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete]
|
38
|
+
# HTTP redirect status codes that this middleware implements
|
39
|
+
REDIRECT_CODES = Set.new [301, 302, 303, 307]
|
40
|
+
# Keys in env hash which will get cleared between requests
|
41
|
+
ENV_TO_CLEAR = Set.new [:status, :response, :response_headers]
|
42
|
+
|
43
|
+
# Default value for max redirects followed
|
44
|
+
FOLLOW_LIMIT = 3
|
45
|
+
|
46
|
+
# Public: Initialize the middleware.
|
47
|
+
#
|
48
|
+
# options - An options Hash (default: {}):
|
49
|
+
# limit - A Numeric redirect limit (default: 3)
|
50
|
+
# standards_compliant - A Boolean indicating whether to respect
|
51
|
+
# the HTTP spec when following 302
|
52
|
+
# (default: false)
|
53
|
+
# cookie - Use either an array of strings
|
54
|
+
# (e.g. ['cookie1', 'cookie2']) to choose kept cookies
|
55
|
+
# or :all to keep all cookies.
|
56
|
+
def initialize(app, options = {})
|
57
|
+
super(app)
|
58
|
+
@options = options
|
59
|
+
|
60
|
+
@replay_request_codes = Set.new [307]
|
61
|
+
@replay_request_codes << 302 if standards_compliant?
|
62
|
+
end
|
63
|
+
|
64
|
+
def call(env)
|
65
|
+
perform_with_redirection(env, follow_limit)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def transform_into_get?(response)
|
71
|
+
return false if [:head, :options].include? response.env[:method]
|
72
|
+
# Never convert head or options to a get. That would just be silly.
|
73
|
+
|
74
|
+
!@replay_request_codes.include? response.status
|
75
|
+
end
|
76
|
+
|
77
|
+
def perform_with_redirection(env, follows)
|
78
|
+
request_body = env[:body]
|
79
|
+
response = @app.call(env)
|
80
|
+
|
81
|
+
response.on_complete do |env|
|
82
|
+
if follow_redirect?(env, response)
|
83
|
+
raise RedirectLimitReached, response if follows.zero?
|
84
|
+
env = update_env(env, request_body, response)
|
85
|
+
response = perform_with_redirection(env, follows - 1)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
response
|
89
|
+
end
|
90
|
+
|
91
|
+
def update_env(env, request_body, response)
|
92
|
+
env[:url] += response['location']
|
93
|
+
if @options[:cookies]
|
94
|
+
cookies = keep_cookies(env)
|
95
|
+
env[:request_headers][:cookies] = cookies unless cookies.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
if transform_into_get?(response)
|
99
|
+
env[:method] = :get
|
100
|
+
env[:body] = nil
|
101
|
+
else
|
102
|
+
env[:body] = request_body
|
103
|
+
end
|
104
|
+
|
105
|
+
ENV_TO_CLEAR.each {|key| env.delete key }
|
106
|
+
|
107
|
+
env
|
108
|
+
end
|
109
|
+
|
110
|
+
def follow_redirect?(env, response)
|
111
|
+
ALLOWED_METHODS.include? env[:method] and
|
112
|
+
REDIRECT_CODES.include? response.status
|
113
|
+
end
|
114
|
+
|
115
|
+
def follow_limit
|
116
|
+
@options.fetch(:limit, FOLLOW_LIMIT)
|
117
|
+
end
|
118
|
+
|
119
|
+
def keep_cookies(env)
|
120
|
+
cookies = @options.fetch(:cookies, [])
|
121
|
+
response_cookies = env[:response_headers][:cookies]
|
122
|
+
cookies == :all ? response_cookies : selected_request_cookies(response_cookies)
|
123
|
+
end
|
124
|
+
|
125
|
+
def selected_request_cookies(cookies)
|
126
|
+
selected_cookies(cookies)[0...-1]
|
127
|
+
end
|
128
|
+
|
129
|
+
def selected_cookies(cookies)
|
130
|
+
"".tap do |cookie_string|
|
131
|
+
@options[:cookies].each do |cookie|
|
132
|
+
string = /#{cookie}=?[^;]*/.match(cookies)[0] + ';'
|
133
|
+
cookie_string << string
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def standards_compliant?
|
139
|
+
@options.fetch(:standards_compliant, false)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'footrest/response_middleware'
|
2
|
+
|
3
|
+
module Footrest
|
4
|
+
# Public: Parse response bodies as JSON.
|
5
|
+
class ParseJson < ResponseMiddleware
|
6
|
+
dependency do
|
7
|
+
require 'json' unless defined?(::JSON)
|
8
|
+
end
|
9
|
+
|
10
|
+
define_parser do |body|
|
11
|
+
::JSON.parse body unless body.strip.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Override the content-type of the response with "application/json"
|
15
|
+
# if the response body looks like it might be JSON, i.e. starts with an
|
16
|
+
# open bracket.
|
17
|
+
#
|
18
|
+
# This is to fix responses from certain API providers that insist on serving
|
19
|
+
# JSON with wrong MIME-types such as "text/javascript".
|
20
|
+
class MimeTypeFix < ResponseMiddleware
|
21
|
+
MIME_TYPE = 'application/json'.freeze
|
22
|
+
|
23
|
+
def process_response(env)
|
24
|
+
old_type = env[:response_headers][CONTENT_TYPE].to_s
|
25
|
+
new_type = MIME_TYPE.dup
|
26
|
+
new_type << ';' << old_type.split(';', 2).last if old_type.index(';')
|
27
|
+
env[:response_headers][CONTENT_TYPE] = new_type
|
28
|
+
end
|
29
|
+
|
30
|
+
BRACKETS = %w- [ { -
|
31
|
+
WHITESPACE = [ " ", "\n", "\r", "\t" ]
|
32
|
+
|
33
|
+
def parse_response?(env)
|
34
|
+
super and BRACKETS.include? first_char(env[:body])
|
35
|
+
end
|
36
|
+
|
37
|
+
def first_char(body)
|
38
|
+
idx = -1
|
39
|
+
begin
|
40
|
+
char = body[idx += 1]
|
41
|
+
char = char.chr if char
|
42
|
+
end while char and WHITESPACE.include? char
|
43
|
+
char
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module Footrest
|
4
|
+
# Internal: The base class for middleware that parses responses.
|
5
|
+
class ResponseMiddleware < Faraday::Middleware
|
6
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :parser
|
10
|
+
end
|
11
|
+
|
12
|
+
# Store a Proc that receives the body and returns the parsed result.
|
13
|
+
def self.define_parser(parser = nil)
|
14
|
+
@parser = parser || Proc.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.inherited(subclass)
|
18
|
+
super
|
19
|
+
subclass.load_error = self.load_error if subclass.respond_to? :load_error=
|
20
|
+
subclass.parser = self.parser
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(app = nil, options = {})
|
24
|
+
super(app)
|
25
|
+
@options = options
|
26
|
+
@content_types = Array(options[:content_type])
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(environment)
|
30
|
+
@app.call(environment).on_complete do |env|
|
31
|
+
if process_response_type?(response_type(env)) and parse_response?(env)
|
32
|
+
process_response(env)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def process_response(env)
|
38
|
+
env[:raw_body] = env[:body] if preserve_raw?(env)
|
39
|
+
env[:body] = parse(env[:body])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse the response body.
|
43
|
+
#
|
44
|
+
# Instead of overriding this method, consider using `define_parser`.
|
45
|
+
def parse(body)
|
46
|
+
if self.class.parser
|
47
|
+
begin
|
48
|
+
self.class.parser.call(body)
|
49
|
+
rescue StandardError, SyntaxError => err
|
50
|
+
raise err if err.is_a? SyntaxError and err.class.name != 'Psych::SyntaxError'
|
51
|
+
raise Faraday::Error::ParsingError, err
|
52
|
+
end
|
53
|
+
else
|
54
|
+
body
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def response_type(env)
|
59
|
+
type = env[:response_headers][CONTENT_TYPE].to_s
|
60
|
+
type = type.split(';', 2).first if type.index(';')
|
61
|
+
type
|
62
|
+
end
|
63
|
+
|
64
|
+
def process_response_type?(type)
|
65
|
+
@content_types.empty? or @content_types.any? { |pattern|
|
66
|
+
pattern.is_a?(Regexp) ? type =~ pattern : type == pattern
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_response?(env)
|
71
|
+
env[:body].respond_to? :to_str
|
72
|
+
end
|
73
|
+
|
74
|
+
def preserve_raw?(env)
|
75
|
+
env[:request].fetch(:preserve_raw, @options[:preserve_raw])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/footrest/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: footrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2014-02-05 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake
|
@@ -110,22 +110,6 @@ dependencies:
|
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: faraday
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
none: false
|
115
|
-
requirements:
|
116
|
-
- - ~>
|
117
|
-
- !ruby/object:Gem::Version
|
118
|
-
version: 0.8.8
|
119
|
-
type: :runtime
|
120
|
-
prerelease: false
|
121
|
-
version_requirements: !ruby/object:Gem::Requirement
|
122
|
-
none: false
|
123
|
-
requirements:
|
124
|
-
- - ~>
|
125
|
-
- !ruby/object:Gem::Version
|
126
|
-
version: 0.8.8
|
127
|
-
- !ruby/object:Gem::Dependency
|
128
|
-
name: faraday_middleware
|
129
113
|
requirement: !ruby/object:Gem::Requirement
|
130
114
|
none: false
|
131
115
|
requirements:
|
@@ -184,9 +168,12 @@ files:
|
|
184
168
|
- footrest.gemspec
|
185
169
|
- lib/footrest/client.rb
|
186
170
|
- lib/footrest/connection.rb
|
171
|
+
- lib/footrest/follow_redirects.rb
|
187
172
|
- lib/footrest/http_error.rb
|
188
173
|
- lib/footrest/pagination.rb
|
174
|
+
- lib/footrest/parse_json.rb
|
189
175
|
- lib/footrest/request.rb
|
176
|
+
- lib/footrest/response_middleware.rb
|
190
177
|
- lib/footrest/version.rb
|
191
178
|
- lib/footrest.rb
|
192
179
|
- spec/footrest/client_spec.rb
|