Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move json middleware (request and response) from faraday_middleware #1300

Merged
merged 6 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/middleware/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ base64 representation.
* [`Multipart`][multipart] converts a `Faraday::Request#body` hash of key/value pairs into a
multipart form request.
* [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body.
* [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a json request body.
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
* [`Json Response`][json-response] parses response body into a hash of key/value pairs.
* [`Retry`][retry] automatically retries requests that fail due to intermittent client
or server errors (such as network hiccups).
* [`Instrumentation`][instrumentation] allows to instrument requests using different tools.
Expand All @@ -44,7 +46,9 @@ before returning it.
[authentication]: ./authentication
[multipart]: ./multipart
[url_encoded]: ./url-encoded
[json-request]: ./json-request
[retry]: ./retry
[instrumentation]: ./instrumentation
[json-response]: ./json-response
[logger]: ./logger
[raise_error]: ./raise-error
4 changes: 2 additions & 2 deletions docs/middleware/request/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ permalink: /middleware/instrumentation
hide: true
prev_name: Retry Middleware
prev_link: ./retry
next_name: Logger Middleware
next_link: ./logger
next_name: Json Response Middleware
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
next_link: ./json-response
top_name: Back to Middleware
top_link: ./list
---
Expand Down
31 changes: 31 additions & 0 deletions docs/middleware/request/json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
layout: documentation
title: "Json Request Middleware"
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
permalink: /middleware/json-request
hide: true
prev_name: UrlEncoded Middleware
prev_link: ./url-encoded
next_name: Retry Middleware
next_link: ./retry
top_name: Back to Middleware
top_link: ./list
---

The `Json Request` middleware converts a `Faraday::Request#body` hash of key/value pairs into a json request body.
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
The middleware also automatically sets the `Content-Type` header to `application/json`,
processes only requests with matching Content-type or those without a type and
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
doesn't try to encode bodies that already are in string form.

### Example Usage

```ruby
conn = Faraday.new(...) do |f|
f.request :json
...
end

conn.post('/', { a: 1, b: 2 })
# POST with
# Content-Type: application/json
# Body: {"a":1,"b":2}
```
4 changes: 2 additions & 2 deletions docs/middleware/request/url_encoded.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ permalink: /middleware/url-encoded
hide: true
prev_name: Multipart Middleware
prev_link: ./multipart
next_name: Retry Middleware
next_link: ./retry
next_name: Json Request Middleware
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
next_link: ./json-request
top_name: Back to Middleware
top_link: ./list
---
Expand Down
29 changes: 29 additions & 0 deletions docs/middleware/response/json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
layout: documentation
title: "Json Response Middleware"
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
permalink: /middleware/json-response
hide: true
prev_name: Instrumentation Middleware
prev_link: ./instrumentation
next_name: Logger Middleware
next_link: ./logger
top_name: Back to Middleware
top_link: ./list
---

The `Json Response` middleware parses response body into a hash of key/value pairs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be referred to like Json::Response here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class is technically Faraday::Response::Json, I could go with that or just JSON and move response outside as an adjective to "middleware"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for the latter (and applied the same change to the request one), but happy to change if you prefer the other

The behaviour can be customized with the following options:
* **parser_options:** options that will be sent to the JSON.parse method. Defaults to {}
* **content_type:** Single value or Array of response content-types that should be processed. Can be either strings or Regex. Defaults to `/\bjson$/`
* **preserve_raw:** If set to true, the original un-parsed response will be stored in the `response.env[:raw_body]` property. Defaults to `false`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should have a period at the end of sentences.


### Example Usage

```ruby
conn = Faraday.new('http://httpbingo.org') do |f|
f.response :json, **options
end

conn.get('json').body
# => {"slideshow"=>{"author"=>"Yours Truly", "date"=>"date of publication", "slides"=>[{"title"=>"Wake up to WonderWidgets!", "type"=>"all"}, {"items"=>["Why <em>WonderWidgets</em> are great", "Who <em>buys</em> WonderWidgets"], "title"=>"Overview", "type"=>"all"}], "title"=>"Sample Slide Show"}}
```
4 changes: 2 additions & 2 deletions docs/middleware/response/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ layout: documentation
title: "Logger Middleware"
permalink: /middleware/logger
hide: true
prev_name: Instrumentation Middleware
prev_link: ./instrumentation
prev_name: Json Response Middleware
iMacTia marked this conversation as resolved.
Show resolved Hide resolved
prev_link: ./json-response
next_name: RaiseError Middleware
next_link: ./raise-error
top_name: Back to Middleware
Expand Down
2 changes: 2 additions & 0 deletions lib/faraday.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
# conn.get '/'
#
module Faraday
CONTENT_TYPE = 'Content-Type'

class << self
# The root path that Faraday is being loaded from.
#
Expand Down
13 changes: 7 additions & 6 deletions lib/faraday/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class Request < Struct.new(
:TokenAuthentication,
'token_authentication'
],
instrumentation: [:Instrumentation, 'instrumentation']
instrumentation: [:Instrumentation, 'instrumentation'],
json: [:Json, 'json']

# @param request_method [String]
# @yield [request] for block customization, if block given
Expand Down Expand Up @@ -140,11 +141,11 @@ def marshal_dump
# @param serialised [Hash] the serialised object.
def marshal_load(serialised)
self.http_method = serialised[:http_method]
self.body = serialised[:body]
self.headers = serialised[:headers]
self.path = serialised[:path]
self.params = serialised[:params]
self.options = serialised[:options]
self.body = serialised[:body]
self.headers = serialised[:headers]
self.path = serialised[:path]
self.params = serialised[:params]
self.options = serialised[:options]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change!

end

# @return [Env] the Env for this Request
Expand Down
53 changes: 53 additions & 0 deletions lib/faraday/request/json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

require 'json'

module Faraday
class Request
# Request middleware that encodes the body as JSON.
#
# Processes only requests with matching Content-type or those without a type.
# If a request doesn't have a type but has a body, it sets the Content-type
# to JSON MIME-type.
#
# Doesn't try to encode bodies that already are in string form.
class Json < Middleware
MIME_TYPE = 'application/json'
MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}.freeze

def on_request(env)
match_content_type(env) do |data|
env[:body] = encode(data)
end
end

private

def encode(data)
::JSON.generate(data)
end

def match_content_type(env)
return unless process_request?(env)

env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE
yield env[:body] unless env[:body].respond_to?(:to_str)
end

def process_request?(env)
type = request_type(env)
body?(env) && (type.empty? || MIME_TYPE_REGEX =~ type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruby version question: Can we use #match? here?

end

def body?(env)
(body = env[:body]) && !(body.respond_to?(:to_str) && body.empty?)
end

def request_type(env)
type = env[:request_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
4 changes: 3 additions & 1 deletion lib/faraday/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def on_complete(env)

register_middleware File.expand_path('response', __dir__),
raise_error: [:RaiseError, 'raise_error'],
logger: [:Logger, 'logger']
logger: [:Logger, 'logger'],
json: [:Json, 'json']

def initialize(env = nil)
@env = Env.from(env) if env
Expand All @@ -42,6 +43,7 @@ def reason_phrase
def headers
finished? ? env.response_headers : {}
end

def_delegator :headers, :[]

def body
Expand Down
52 changes: 52 additions & 0 deletions lib/faraday/response/json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require 'json'

module Faraday
class Response
# Parse response bodies as JSON.
class Json < Middleware
def initialize(app = nil, parser_options: nil, content_type: /\bjson$/, preserve_raw: false)
super(app)
@parser_options = parser_options
@content_types = Array(content_type)
@preserve_raw = preserve_raw
end

def on_complete(env)
process_response(env) if parse_response?(env)
end

private

def process_response(env)
env[:raw_body] = env[:body] if @preserve_raw
env[:body] = parse(env[:body])
rescue StandardError, SyntaxError => e
raise Faraday::ParsingError.new(e, env[:response])
end

def parse(body)
::JSON.parse(body, @parser_options || {}) unless body.strip.empty?
end

def parse_response?(env)
process_response_type?(env) &&
env[:body].respond_to?(:to_str)
end

def process_response_type?(env)
type = response_type(env)
@content_types.empty? || @content_types.any? do |pattern|
pattern.is_a?(Regexp) ? type =~ pattern : type == pattern
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, perhaps too, an opportunity for #match?

end
end

def response_type(env)
type = env[:response_headers][CONTENT_TYPE].to_s
type = type.split(';', 2).first if type.index(';')
type
end
end
end
end
6 changes: 2 additions & 4 deletions lib/faraday/response/logger.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'forwardable'
require 'logger'
require 'faraday/logging/formatter'

module Faraday
Expand All @@ -11,10 +12,7 @@ class Response
class Logger < Middleware
def initialize(app, logger = nil, options = {})
super(app)
logger ||= begin
require 'logger'
::Logger.new($stdout)
end
logger ||= ::Logger.new($stdout)
formatter_class = options.delete(:formatter) || Logging::Formatter
@formatter = formatter_class.new(logger: logger, options: options)
yield @formatter if block_given?
Expand Down
Loading