Skip to content

Commit 4a838a5

Browse files
authored
Add JSON middleware (#1400)
1 parent 47b3979 commit 4a838a5

File tree

14 files changed

+423
-17
lines changed

14 files changed

+423
-17
lines changed

docs/middleware/list.md

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ base64 representation.
2727
* [`Multipart`][multipart] converts a `Faraday::Request#body` hash of key/value pairs into a
2828
multipart form request.
2929
* [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body.
30+
* [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body.
31+
* [`Json Response`][json-response] parses response body into a hash of key/value pairs.
3032
* [`Retry`][retry] automatically retries requests that fail due to intermittent client
3133
or server errors (such as network hiccups).
3234
* [`Instrumentation`][instrumentation] allows to instrument requests using different tools.
@@ -44,7 +46,9 @@ before returning it.
4446
[authentication]: ./authentication
4547
[multipart]: ./multipart
4648
[url_encoded]: ./url-encoded
49+
[json-request]: ./json-request
4750
[retry]: ./retry
4851
[instrumentation]: ./instrumentation
52+
[json-response]: ./json-response
4953
[logger]: ./logger
5054
[raise_error]: ./raise-error

docs/middleware/request/instrumentation.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ permalink: /middleware/instrumentation
55
hide: true
66
prev_name: Retry Middleware
77
prev_link: ./retry
8-
next_name: Logger Middleware
9-
next_link: ./logger
8+
next_name: JSON Response Middleware
9+
next_link: ./json-response
1010
top_name: Back to Middleware
1111
top_link: ./list
1212
---

docs/middleware/request/json.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
layout: documentation
3+
title: "JSON Request Middleware"
4+
permalink: /middleware/json-request
5+
hide: true
6+
prev_name: UrlEncoded Middleware
7+
prev_link: ./url-encoded
8+
next_name: Retry Middleware
9+
next_link: ./retry
10+
top_name: Back to Middleware
11+
top_link: ./list
12+
---
13+
14+
The `JSON` request middleware converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body.
15+
The middleware also automatically sets the `Content-Type` header to `application/json`,
16+
processes only requests with matching Content-Type or those without a type and
17+
doesn't try to encode bodies that already are in string form.
18+
19+
### Example Usage
20+
21+
```ruby
22+
conn = Faraday.new(...) do |f|
23+
f.request :json
24+
...
25+
end
26+
27+
conn.post('/', { a: 1, b: 2 })
28+
# POST with
29+
# Content-Type: application/json
30+
# Body: {"a":1,"b":2}
31+
```

docs/middleware/request/url_encoded.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ permalink: /middleware/url-encoded
55
hide: true
66
prev_name: Multipart Middleware
77
prev_link: ./multipart
8-
next_name: Retry Middleware
9-
next_link: ./retry
8+
next_name: JSON Request Middleware
9+
next_link: ./json-request
1010
top_name: Back to Middleware
1111
top_link: ./list
1212
---

docs/middleware/response/json.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
layout: documentation
3+
title: "JSON Response Middleware"
4+
permalink: /middleware/json-response
5+
hide: true
6+
prev_name: Instrumentation Middleware
7+
prev_link: ./instrumentation
8+
next_name: Logger Middleware
9+
next_link: ./logger
10+
top_name: Back to Middleware
11+
top_link: ./list
12+
---
13+
14+
The `JSON` response middleware parses response body into a hash of key/value pairs.
15+
The behaviour can be customized with the following options:
16+
* **parser_options:** options that will be sent to the JSON.parse method. Defaults to {}.
17+
* **content_type:** Single value or Array of response content-types that should be processed. Can be either strings or Regex. Defaults to `/\bjson$/`.
18+
* **preserve_raw:** If set to true, the original un-parsed response will be stored in the `response.env[:raw_body]` property. Defaults to `false`.
19+
20+
### Example Usage
21+
22+
```ruby
23+
conn = Faraday.new('http://httpbingo.org') do |f|
24+
f.response :json, **options
25+
end
26+
27+
conn.get('json').body
28+
# => {"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"}}
29+
```

docs/middleware/response/logger.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ layout: documentation
33
title: "Logger Middleware"
44
permalink: /middleware/logger
55
hide: true
6-
prev_name: Instrumentation Middleware
7-
prev_link: ./instrumentation
6+
prev_name: JSON Response Middleware
7+
prev_link: ./json-response
88
next_name: RaiseError Middleware
99
next_link: ./raise-error
1010
top_name: Back to Middleware

lib/faraday.rb

+2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
# conn.get '/'
5858
#
5959
module Faraday
60+
CONTENT_TYPE = 'Content-Type'
61+
6062
class << self
6163
# The root path that Faraday is being loaded from.
6264
#

lib/faraday/request.rb

+7-6
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class Request < Struct.new(
4444
:TokenAuthentication,
4545
'token_authentication'
4646
],
47-
instrumentation: [:Instrumentation, 'instrumentation']
47+
instrumentation: [:Instrumentation, 'instrumentation'],
48+
json: [:Json, 'json']
4849

4950
# @param request_method [String]
5051
# @yield [request] for block customization, if block given
@@ -138,11 +139,11 @@ def marshal_dump
138139
# @param serialised [Hash] the serialised object.
139140
def marshal_load(serialised)
140141
self.http_method = serialised[:http_method]
141-
self.body = serialised[:body]
142-
self.headers = serialised[:headers]
143-
self.path = serialised[:path]
144-
self.params = serialised[:params]
145-
self.options = serialised[:options]
142+
self.body = serialised[:body]
143+
self.headers = serialised[:headers]
144+
self.path = serialised[:path]
145+
self.params = serialised[:params]
146+
self.options = serialised[:options]
146147
end
147148

148149
# @return [Env] the Env for this Request

lib/faraday/request/json.rb

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
module Faraday
6+
class Request
7+
# Request middleware that encodes the body as JSON.
8+
#
9+
# Processes only requests with matching Content-type or those without a type.
10+
# If a request doesn't have a type but has a body, it sets the Content-type
11+
# to JSON MIME-type.
12+
#
13+
# Doesn't try to encode bodies that already are in string form.
14+
class Json < Middleware
15+
MIME_TYPE = 'application/json'
16+
MIME_TYPE_REGEX = %r{^application/(vnd\..+\+)?json$}.freeze
17+
18+
def on_request(env)
19+
match_content_type(env) do |data|
20+
env[:body] = encode(data)
21+
end
22+
end
23+
24+
private
25+
26+
def encode(data)
27+
::JSON.generate(data)
28+
end
29+
30+
def match_content_type(env)
31+
return unless process_request?(env)
32+
33+
env[:request_headers][CONTENT_TYPE] ||= MIME_TYPE
34+
yield env[:body] unless env[:body].respond_to?(:to_str)
35+
end
36+
37+
def process_request?(env)
38+
type = request_type(env)
39+
body?(env) && (type.empty? || type.match?(MIME_TYPE_REGEX))
40+
end
41+
42+
def body?(env)
43+
(body = env[:body]) && !(body.respond_to?(:to_str) && body.empty?)
44+
end
45+
46+
def request_type(env)
47+
type = env[:request_headers][CONTENT_TYPE].to_s
48+
type = type.split(';', 2).first if type.index(';')
49+
type
50+
end
51+
end
52+
end
53+
end
54+
55+
Faraday::Request.register_middleware(json: Faraday::Request::Json)

lib/faraday/response.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ def on_complete(env)
2222

2323
register_middleware File.expand_path('response', __dir__),
2424
raise_error: [:RaiseError, 'raise_error'],
25-
logger: [:Logger, 'logger']
25+
logger: [:Logger, 'logger'],
26+
json: [:Json, 'json']
2627

2728
def initialize(env = nil)
2829
@env = Env.from(env) if env
@@ -42,6 +43,7 @@ def reason_phrase
4243
def headers
4344
finished? ? env.response_headers : {}
4445
end
46+
4547
def_delegator :headers, :[]
4648

4749
def body

lib/faraday/response/json.rb

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require 'json'
4+
5+
module Faraday
6+
class Response
7+
# Parse response bodies as JSON.
8+
class Json < Middleware
9+
def initialize(app = nil, options = {})
10+
super(app)
11+
@parser_options = options[:parser_options]
12+
@content_types = Array(options[:content_type] || /\bjson$/)
13+
@preserve_raw = options[:preserve_raw]
14+
end
15+
16+
def on_complete(env)
17+
process_response(env) if parse_response?(env)
18+
end
19+
20+
private
21+
22+
def process_response(env)
23+
env[:raw_body] = env[:body] if @preserve_raw
24+
env[:body] = parse(env[:body])
25+
rescue StandardError, SyntaxError => e
26+
raise Faraday::ParsingError.new(e, env[:response])
27+
end
28+
29+
def parse(body)
30+
::JSON.parse(body, @parser_options || {}) unless body.strip.empty?
31+
end
32+
33+
def parse_response?(env)
34+
process_response_type?(env) &&
35+
env[:body].respond_to?(:to_str)
36+
end
37+
38+
def process_response_type?(env)
39+
type = response_type(env)
40+
@content_types.empty? || @content_types.any? do |pattern|
41+
pattern.is_a?(Regexp) ? type.match?(pattern) : type == pattern
42+
end
43+
end
44+
45+
def response_type(env)
46+
type = env[:response_headers][CONTENT_TYPE].to_s
47+
type = type.split(';', 2).first if type.index(';')
48+
type
49+
end
50+
end
51+
end
52+
end
53+
54+
Faraday::Response.register_middleware(json: Faraday::Response::Json)

lib/faraday/response/logger.rb

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'forwardable'
4+
require 'logger'
45
require 'faraday/logging/formatter'
56

67
module Faraday
@@ -11,10 +12,7 @@ class Response
1112
class Logger < Middleware
1213
def initialize(app, logger = nil, options = {})
1314
super(app)
14-
logger ||= begin
15-
require 'logger'
16-
::Logger.new($stdout)
17-
end
15+
logger ||= ::Logger.new($stdout)
1816
formatter_class = options.delete(:formatter) || Logging::Formatter
1917
@formatter = formatter_class.new(logger: logger, options: options)
2018
yield @formatter if block_given?

0 commit comments

Comments
 (0)