footrest 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|