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