footrest 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.8.8"
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
@@ -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 FaradayMiddleware::FollowRedirects
18
- faraday.use FaradayMiddleware::ParseJson, :content_type => /\bjson$/
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
@@ -1,3 +1,3 @@
1
1
  module Footrest
2
- VERSION = '0.2.2' unless defined?(Footrest::VERSION)
2
+ VERSION = '0.3.0' unless defined?(Footrest::VERSION)
3
3
  end
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.2.2
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: 2013-10-03 00:00:00.000000000 Z
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