Skip to content

Commit

Permalink
Merge 13c077e into 08b7d4d
Browse files Browse the repository at this point in the history
  • Loading branch information
iMacTia committed Aug 9, 2021
2 parents 08b7d4d + 13c077e commit 2bea546
Show file tree
Hide file tree
Showing 14 changed files with 416 additions and 15 deletions.
4 changes: 4 additions & 0 deletions docs/middleware/list.md
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.
* [`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
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
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
@@ -0,0 +1,31 @@
---
layout: documentation
title: "JSON Request Middleware"
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.
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
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
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
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
@@ -0,0 +1,29 @@
---
layout: documentation
title: "JSON Response Middleware"
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.
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`

### 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
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
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
Expand Up @@ -33,6 +33,8 @@
# conn.get '/'
#
module Faraday
CONTENT_TYPE = 'Content-Type'

class << self
# The root path that Faraday is being loaded from.
#
Expand Down
10 changes: 5 additions & 5 deletions lib/faraday/request.rb
Expand Up @@ -123,11 +123,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]
end

# @return [Env] the Env for this Request
Expand Down
53 changes: 53 additions & 0 deletions lib/faraday/request/json.rb
@@ -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)
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
1 change: 1 addition & 0 deletions lib/faraday/response.rb
Expand Up @@ -26,6 +26,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
@@ -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
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
@@ -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
111 changes: 111 additions & 0 deletions spec/faraday/request/json_spec.rb
@@ -0,0 +1,111 @@
# frozen_string_literal: true

RSpec.describe Faraday::Request::Json do
let(:middleware) { described_class.new(->(env) { Faraday::Response.new(env) }) }

def process(body, content_type = nil)
env = { body: body, request_headers: Faraday::Utils::Headers.new }
env[:request_headers]['content-type'] = content_type if content_type
middleware.call(Faraday::Env.from(env)).env
end

def result_body
result[:body]
end

def result_type
result[:request_headers]['content-type']
end

context 'no body' do
let(:result) { process(nil) }

it "doesn't change body" do
expect(result_body).to be_nil
end

it "doesn't add content type" do
expect(result_type).to be_nil
end
end

context 'empty body' do
let(:result) { process('') }

it "doesn't change body" do
expect(result_body).to be_empty
end

it "doesn't add content type" do
expect(result_type).to be_nil
end
end

context 'string body' do
let(:result) { process('{"a":1}') }

it "doesn't change body" do
expect(result_body).to eq('{"a":1}')
end

it 'adds content type' do
expect(result_type).to eq('application/json')
end
end

context 'object body' do
let(:result) { process(a: 1) }

it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end

it 'adds content type' do
expect(result_type).to eq('application/json')
end
end

context 'empty object body' do
let(:result) { process({}) }

it 'encodes body' do
expect(result_body).to eq('{}')
end
end

context 'object body with json type' do
let(:result) { process({ a: 1 }, 'application/json; charset=utf-8') }

it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end

it "doesn't change content type" do
expect(result_type).to eq('application/json; charset=utf-8')
end
end

context 'object body with vendor json type' do
let(:result) { process({ a: 1 }, 'application/vnd.myapp.v1+json; charset=utf-8') }

it 'encodes body' do
expect(result_body).to eq('{"a":1}')
end

it "doesn't change content type" do
expect(result_type).to eq('application/vnd.myapp.v1+json; charset=utf-8')
end
end

context 'object body with incompatible type' do
let(:result) { process({ a: 1 }, 'application/xml; charset=utf-8') }

it "doesn't change body" do
expect(result_body).to eq(a: 1)
end

it "doesn't change content type" do
expect(result_type).to eq('application/xml; charset=utf-8')
end
end
end

0 comments on commit 2bea546

Please sign in to comment.