This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'date'
|
||||
require 'set'
|
||||
require 'forwardable'
|
||||
require 'faraday/version'
|
||||
require 'faraday/methods'
|
||||
require 'faraday/error'
|
||||
require 'faraday/middleware_registry'
|
||||
require 'faraday/utils'
|
||||
require 'faraday/options'
|
||||
require 'faraday/connection'
|
||||
require 'faraday/rack_builder'
|
||||
require 'faraday/parameters'
|
||||
require 'faraday/middleware'
|
||||
require 'faraday/adapter'
|
||||
require 'faraday/request'
|
||||
require 'faraday/response'
|
||||
require 'faraday/net_http'
|
||||
# This is the main namespace for Faraday.
|
||||
#
|
||||
# It provides methods to create {Connection} objects, and HTTP-related
|
||||
# methods to use directly.
|
||||
#
|
||||
# @example Helpful class methods for easy usage
|
||||
# Faraday.get "http://faraday.com"
|
||||
#
|
||||
# @example Helpful class method `.new` to create {Connection} objects.
|
||||
# conn = Faraday.new "http://faraday.com"
|
||||
# conn.get '/'
|
||||
#
|
||||
module Faraday
|
||||
CONTENT_TYPE = 'Content-Type'
|
||||
|
||||
class << self
|
||||
# The root path that Faraday is being loaded from.
|
||||
#
|
||||
# This is the root from where the libraries are auto-loaded.
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :root_path
|
||||
|
||||
# Gets or sets the path that the Faraday libs are loaded from.
|
||||
# @return [String]
|
||||
attr_accessor :lib_path
|
||||
|
||||
# @overload default_adapter
|
||||
# Gets the Symbol key identifying a default Adapter to use
|
||||
# for the default {Faraday::Connection}. Defaults to `:net_http`.
|
||||
# @return [Symbol] the default adapter
|
||||
# @overload default_adapter=(adapter)
|
||||
# Updates default adapter while resetting {.default_connection}.
|
||||
# @return [Symbol] the new default_adapter.
|
||||
attr_reader :default_adapter
|
||||
|
||||
# Option for the default_adapter
|
||||
# @return [Hash] default_adapter options
|
||||
attr_accessor :default_adapter_options
|
||||
|
||||
# Documented below, see default_connection
|
||||
attr_writer :default_connection
|
||||
|
||||
# Tells Faraday to ignore the environment proxy (http_proxy).
|
||||
# Defaults to `false`.
|
||||
# @return [Boolean]
|
||||
attr_accessor :ignore_env_proxy
|
||||
|
||||
# Initializes a new {Connection}.
|
||||
#
|
||||
# @param url [String,Hash] The optional String base URL to use as a prefix
|
||||
# for all requests. Can also be the options Hash. Any of these
|
||||
# values will be set on every request made, unless overridden
|
||||
# for a specific request.
|
||||
# @param options [Hash]
|
||||
# @option options [String] :url Base URL
|
||||
# @option options [Hash] :params Hash of unencoded URI query params.
|
||||
# @option options [Hash] :headers Hash of unencoded HTTP headers.
|
||||
# @option options [Hash] :request Hash of request options.
|
||||
# @option options [Hash] :ssl Hash of SSL options.
|
||||
# @option options [Hash] :proxy Hash of Proxy options.
|
||||
# @return [Faraday::Connection]
|
||||
#
|
||||
# @example With an URL argument
|
||||
# Faraday.new 'http://faraday.com'
|
||||
# # => Faraday::Connection to http://faraday.com
|
||||
#
|
||||
# @example With an URL argument and an options hash
|
||||
# Faraday.new 'http://faraday.com', params: { page: 1 }
|
||||
# # => Faraday::Connection to http://faraday.com?page=1
|
||||
#
|
||||
# @example With everything in an options hash
|
||||
# Faraday.new url: 'http://faraday.com',
|
||||
# params: { page: 1 }
|
||||
# # => Faraday::Connection to http://faraday.com?page=1
|
||||
def new(url = nil, options = {}, &block)
|
||||
options = Utils.deep_merge(default_connection_options, options)
|
||||
Faraday::Connection.new(url, options, &block)
|
||||
end
|
||||
|
||||
# Documented elsewhere, see default_adapter reader
|
||||
def default_adapter=(adapter)
|
||||
@default_connection = nil
|
||||
@default_adapter = adapter
|
||||
end
|
||||
|
||||
def respond_to_missing?(symbol, include_private = false)
|
||||
default_connection.respond_to?(symbol, include_private) || super
|
||||
end
|
||||
|
||||
# @overload default_connection
|
||||
# Gets the default connection used for simple scripts.
|
||||
# @return [Faraday::Connection] a connection configured with
|
||||
# the default_adapter.
|
||||
# @overload default_connection=(connection)
|
||||
# @param connection [Faraday::Connection]
|
||||
# Sets the default {Faraday::Connection} for simple scripts that
|
||||
# access the Faraday constant directly, such as
|
||||
# <code>Faraday.get "https://faraday.com"</code>.
|
||||
def default_connection
|
||||
@default_connection ||= Connection.new(default_connection_options)
|
||||
end
|
||||
|
||||
# Gets the default connection options used when calling {Faraday#new}.
|
||||
#
|
||||
# @return [Faraday::ConnectionOptions]
|
||||
def default_connection_options
|
||||
@default_connection_options ||= ConnectionOptions.new
|
||||
end
|
||||
|
||||
# Sets the default options used when calling {Faraday#new}.
|
||||
#
|
||||
# @param options [Hash, Faraday::ConnectionOptions]
|
||||
def default_connection_options=(options)
|
||||
@default_connection = nil
|
||||
@default_connection_options = ConnectionOptions.from(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Internal: Proxies method calls on the Faraday constant to
|
||||
# .default_connection.
|
||||
def method_missing(name, *args, &block)
|
||||
if default_connection.respond_to?(name)
|
||||
default_connection.send(name, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.ignore_env_proxy = false
|
||||
self.root_path = File.expand_path __dir__
|
||||
self.lib_path = File.expand_path 'faraday', __dir__
|
||||
self.default_adapter = :net_http
|
||||
self.default_adapter_options = {}
|
||||
end
|
||||
@@ -0,0 +1,101 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Base class for all Faraday adapters. Adapters are
|
||||
# responsible for fulfilling a Faraday request.
|
||||
class Adapter
|
||||
extend MiddlewareRegistry
|
||||
|
||||
CONTENT_LENGTH = 'Content-Length'
|
||||
|
||||
# This module marks an Adapter as supporting parallel requests.
|
||||
module Parallelism
|
||||
attr_writer :supports_parallel
|
||||
|
||||
def supports_parallel?
|
||||
@supports_parallel
|
||||
end
|
||||
|
||||
def inherited(subclass)
|
||||
super
|
||||
subclass.supports_parallel = supports_parallel?
|
||||
end
|
||||
end
|
||||
|
||||
extend Parallelism
|
||||
self.supports_parallel = false
|
||||
|
||||
def initialize(_app = nil, opts = {}, &block)
|
||||
@app = ->(env) { env.response }
|
||||
@connection_options = opts
|
||||
@config_block = block
|
||||
end
|
||||
|
||||
# Yields or returns an adapter's configured connection. Depends on
|
||||
# #build_connection being defined on this adapter.
|
||||
#
|
||||
# @param env [Faraday::Env, Hash] The env object for a faraday request.
|
||||
#
|
||||
# @return The return value of the given block, or the HTTP connection object
|
||||
# if no block is given.
|
||||
def connection(env)
|
||||
conn = build_connection(env)
|
||||
return conn unless block_given?
|
||||
|
||||
yield conn
|
||||
end
|
||||
|
||||
# Close any persistent connections. The adapter should still be usable
|
||||
# after calling close.
|
||||
def close
|
||||
# Possible implementation:
|
||||
# @app.close if @app.respond_to?(:close)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env.clear_body if env.needs_body?
|
||||
env.response = Response.new
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_response(env, status, body, headers = nil, reason_phrase = nil, finished: true)
|
||||
env.status = status
|
||||
env.body = body
|
||||
env.reason_phrase = reason_phrase&.to_s&.strip
|
||||
env.response_headers = Utils::Headers.new.tap do |response_headers|
|
||||
response_headers.update headers unless headers.nil?
|
||||
yield(response_headers) if block_given?
|
||||
end
|
||||
|
||||
env.response.finish(env) unless env.parallel? || !finished
|
||||
env.response
|
||||
end
|
||||
|
||||
# Fetches either a read, write, or open timeout setting. Defaults to the
|
||||
# :timeout value if a more specific one is not given.
|
||||
#
|
||||
# @param type [Symbol] Describes which timeout setting to get: :read,
|
||||
# :write, or :open.
|
||||
# @param options [Hash] Hash containing Symbol keys like :timeout,
|
||||
# :read_timeout, :write_timeout, or :open_timeout
|
||||
#
|
||||
# @return [Integer, nil] Timeout duration in seconds, or nil if no timeout
|
||||
# has been set.
|
||||
def request_timeout(type, options)
|
||||
key = TIMEOUT_KEYS.fetch(type) do
|
||||
msg = "Expected :read, :write, :open. Got #{type.inspect} :("
|
||||
raise ArgumentError, msg
|
||||
end
|
||||
options[key] || options[:timeout]
|
||||
end
|
||||
|
||||
TIMEOUT_KEYS = {
|
||||
read: :read_timeout,
|
||||
open: :open_timeout,
|
||||
write: :write_timeout
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
|
||||
require 'faraday/adapter/test'
|
||||
@@ -0,0 +1,311 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'timeout'
|
||||
|
||||
module Faraday
|
||||
class Adapter
|
||||
# @example
|
||||
# test = Faraday::Connection.new do
|
||||
# use Faraday::Adapter::Test do |stub|
|
||||
# # Define matcher to match the request
|
||||
# stub.get '/resource.json' do
|
||||
# # return static content
|
||||
# [200, {'Content-Type' => 'application/json'}, 'hi world']
|
||||
# end
|
||||
#
|
||||
# # response with content generated based on request
|
||||
# stub.get '/showget' do |env|
|
||||
# [200, {'Content-Type' => 'text/plain'}, env[:method].to_s]
|
||||
# end
|
||||
#
|
||||
# # A regular expression can be used as matching filter
|
||||
# stub.get /\A\/items\/(\d+)\z/ do |env, meta|
|
||||
# # in case regular expression is used, an instance of MatchData
|
||||
# # can be received
|
||||
# [200,
|
||||
# {'Content-Type' => 'text/plain'},
|
||||
# "showing item: #{meta[:match_data][1]}"
|
||||
# ]
|
||||
# end
|
||||
#
|
||||
# # Test the request body is the same as the stubbed body
|
||||
# stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] }
|
||||
#
|
||||
# # You can pass a proc as a stubbed body and check the request body in your way.
|
||||
# # In this case, the proc should return true or false.
|
||||
# stub.post('/foo', ->(request_body) do
|
||||
# JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, '']
|
||||
# end
|
||||
#
|
||||
# # You can set strict_mode to exactly match the stubbed requests.
|
||||
# stub.strict_mode = true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# resp = test.get '/resource.json'
|
||||
# resp.body # => 'hi world'
|
||||
#
|
||||
# resp = test.get '/showget'
|
||||
# resp.body # => 'get'
|
||||
#
|
||||
# resp = test.get '/items/1'
|
||||
# resp.body # => 'showing item: 1'
|
||||
#
|
||||
# resp = test.get '/items/2'
|
||||
# resp.body # => 'showing item: 2'
|
||||
#
|
||||
# resp = test.post '/bar', 'name=YK&word=call'
|
||||
# resp.status # => 200
|
||||
#
|
||||
# resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now)
|
||||
# resp.status # => 200
|
||||
class Test < Faraday::Adapter
|
||||
attr_accessor :stubs
|
||||
|
||||
# A stack of Stubs
|
||||
class Stubs
|
||||
class NotFound < StandardError
|
||||
end
|
||||
|
||||
def initialize(strict_mode: false)
|
||||
# { get: [Stub, Stub] }
|
||||
@stack = {}
|
||||
@consumed = {}
|
||||
@strict_mode = strict_mode
|
||||
@stubs_mutex = Monitor.new
|
||||
yield(self) if block_given?
|
||||
end
|
||||
|
||||
def empty?
|
||||
@stack.empty?
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def match(env)
|
||||
request_method = env[:method]
|
||||
return false unless @stack.key?(request_method)
|
||||
|
||||
stack = @stack[request_method]
|
||||
consumed = (@consumed[request_method] ||= [])
|
||||
|
||||
@stubs_mutex.synchronize do
|
||||
stub, meta = matches?(stack, env)
|
||||
if stub
|
||||
removed = stack.delete(stub)
|
||||
consumed << removed unless removed.nil?
|
||||
return stub, meta
|
||||
end
|
||||
end
|
||||
matches?(consumed, env)
|
||||
end
|
||||
|
||||
def get(path, headers = {}, &block)
|
||||
new_stub(:get, path, headers, &block)
|
||||
end
|
||||
|
||||
def head(path, headers = {}, &block)
|
||||
new_stub(:head, path, headers, &block)
|
||||
end
|
||||
|
||||
def post(path, body = nil, headers = {}, &block)
|
||||
new_stub(:post, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def put(path, body = nil, headers = {}, &block)
|
||||
new_stub(:put, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def patch(path, body = nil, headers = {}, &block)
|
||||
new_stub(:patch, path, headers, body, &block)
|
||||
end
|
||||
|
||||
def delete(path, headers = {}, &block)
|
||||
new_stub(:delete, path, headers, &block)
|
||||
end
|
||||
|
||||
def options(path, headers = {}, &block)
|
||||
new_stub(:options, path, headers, &block)
|
||||
end
|
||||
|
||||
# Raises an error if any of the stubbed calls have not been made.
|
||||
def verify_stubbed_calls
|
||||
failed_stubs = []
|
||||
@stack.each do |method, stubs|
|
||||
next if stubs.empty?
|
||||
|
||||
failed_stubs.concat(
|
||||
stubs.map do |stub|
|
||||
"Expected #{method} #{stub}."
|
||||
end
|
||||
)
|
||||
end
|
||||
raise failed_stubs.join(' ') unless failed_stubs.empty?
|
||||
end
|
||||
|
||||
# Set strict_mode. If the value is true, this adapter tries to find matched requests strictly,
|
||||
# which means that all of a path, parameters, and headers must be the same as an actual request.
|
||||
def strict_mode=(value)
|
||||
@strict_mode = value
|
||||
@stack.each_value do |stubs|
|
||||
stubs.each do |stub|
|
||||
stub.strict_mode = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def new_stub(request_method, path, headers = {}, body = nil, &block)
|
||||
normalized_path, host =
|
||||
if path.is_a?(Regexp)
|
||||
path
|
||||
else
|
||||
[
|
||||
Faraday::Utils.normalize_path(path),
|
||||
Faraday::Utils.URI(path).host
|
||||
]
|
||||
end
|
||||
path, query = normalized_path.respond_to?(:split) ? normalized_path.split('?') : normalized_path
|
||||
headers = Utils::Headers.new(headers)
|
||||
|
||||
stub = Stub.new(host, path, query, headers, body, @strict_mode, block)
|
||||
(@stack[request_method] ||= []) << stub
|
||||
end
|
||||
|
||||
# @param stack [Hash]
|
||||
# @param env [Faraday::Env]
|
||||
def matches?(stack, env)
|
||||
stack.each do |stub|
|
||||
match_result, meta = stub.matches?(env)
|
||||
return stub, meta if match_result
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Stub request
|
||||
Stub = Struct.new(:host, :path, :query, :headers, :body, :strict_mode, :block) do
|
||||
# @param env [Faraday::Env]
|
||||
def matches?(env)
|
||||
request_host = env[:url].host
|
||||
request_path = Faraday::Utils.normalize_path(env[:url].path)
|
||||
request_headers = env.request_headers
|
||||
request_body = env[:body]
|
||||
|
||||
# meta is a hash used as carrier
|
||||
# that will be yielded to consumer block
|
||||
meta = {}
|
||||
[(host.nil? || host == request_host) &&
|
||||
path_match?(request_path, meta) &&
|
||||
params_match?(env) &&
|
||||
body_match?(request_body) &&
|
||||
headers_match?(request_headers), meta]
|
||||
end
|
||||
|
||||
def path_match?(request_path, meta)
|
||||
if path.is_a?(Regexp)
|
||||
!!(meta[:match_data] = path.match(request_path))
|
||||
else
|
||||
path == request_path
|
||||
end
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def params_match?(env)
|
||||
request_params = env[:params]
|
||||
params = env.params_encoder.decode(query) || {}
|
||||
|
||||
if strict_mode
|
||||
return Set.new(params) == Set.new(request_params)
|
||||
end
|
||||
|
||||
params.keys.all? do |key|
|
||||
request_params[key] == params[key]
|
||||
end
|
||||
end
|
||||
|
||||
def headers_match?(request_headers)
|
||||
if strict_mode
|
||||
headers_with_user_agent = headers.dup.tap do |hs|
|
||||
# NOTE: Set User-Agent in case it's not set when creating Stubs.
|
||||
# Users would not want to set Faraday's User-Agent explicitly.
|
||||
hs[:user_agent] ||= Connection::USER_AGENT
|
||||
end
|
||||
return Set.new(headers_with_user_agent) == Set.new(request_headers)
|
||||
end
|
||||
|
||||
headers.keys.all? do |key|
|
||||
request_headers[key] == headers[key]
|
||||
end
|
||||
end
|
||||
|
||||
def body_match?(request_body)
|
||||
return true if body.to_s.empty?
|
||||
|
||||
case body
|
||||
when Proc
|
||||
body.call(request_body)
|
||||
else
|
||||
request_body == body
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{path} #{body}"
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app, stubs = nil, &block)
|
||||
super(app)
|
||||
@stubs = stubs || Stubs.new
|
||||
configure(&block) if block
|
||||
end
|
||||
|
||||
def configure
|
||||
yield(stubs)
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def call(env)
|
||||
super
|
||||
|
||||
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
|
||||
env[:params] = env.params_encoder.decode(env[:url].query) || {}
|
||||
stub, meta = stubs.match(env)
|
||||
|
||||
unless stub
|
||||
raise Stubs::NotFound, "no stubbed request for #{env[:method]} " \
|
||||
"#{env[:url]} #{env[:body]} #{env[:headers]}"
|
||||
end
|
||||
|
||||
block_arity = stub.block.arity
|
||||
params = if block_arity >= 0
|
||||
[env, meta].take(block_arity)
|
||||
else
|
||||
[env, meta]
|
||||
end
|
||||
|
||||
timeout = request_timeout(:open, env[:request])
|
||||
timeout ||= request_timeout(:read, env[:request])
|
||||
|
||||
status, headers, body =
|
||||
if timeout
|
||||
::Timeout.timeout(timeout, Faraday::TimeoutError) do
|
||||
stub.block.call(*params)
|
||||
end
|
||||
else
|
||||
stub.block.call(*params)
|
||||
end
|
||||
|
||||
# We need to explicitly pass `reason_phrase = nil` here to avoid keyword args conflicts.
|
||||
# See https://github.com/lostisland/faraday/issues/1444
|
||||
# TODO: remove `nil` explicit reason_phrase once Ruby 3.0 becomes minimum req. version
|
||||
save_response(env, status, body, headers, nil)
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Adapter.register_middleware(test: Faraday::Adapter::Test)
|
||||
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'monitor'
|
||||
|
||||
module Faraday
|
||||
# AdapterRegistry registers adapter class names so they can be looked up by a
|
||||
# String or Symbol name.
|
||||
class AdapterRegistry
|
||||
def initialize
|
||||
@lock = Monitor.new
|
||||
@constants = {}
|
||||
end
|
||||
|
||||
def get(name)
|
||||
klass = @lock.synchronize do
|
||||
@constants[name]
|
||||
end
|
||||
return klass if klass
|
||||
|
||||
Object.const_get(name).tap { |c| set(c, name) }
|
||||
end
|
||||
|
||||
def set(klass, name = nil)
|
||||
name ||= klass.to_s
|
||||
@lock.synchronize do
|
||||
@constants[name] = klass
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,555 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Connection objects manage the default properties and the middleware
|
||||
# stack for fulfilling an HTTP request.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# conn = Faraday::Connection.new 'http://httpbingo.org'
|
||||
#
|
||||
# # GET http://httpbingo.org/nigiri
|
||||
# conn.get 'nigiri'
|
||||
# # => #<Faraday::Response>
|
||||
#
|
||||
class Connection
|
||||
# A Set of allowed HTTP verbs.
|
||||
METHODS = Set.new %i[get post put delete head patch options trace]
|
||||
USER_AGENT = "Faraday v#{VERSION}".freeze
|
||||
|
||||
# @return [Hash] URI query unencoded key/value pairs.
|
||||
attr_reader :params
|
||||
|
||||
# @return [Hash] unencoded HTTP header key/value pairs.
|
||||
attr_reader :headers
|
||||
|
||||
# @return [String] a URI with the prefix used for all requests from this
|
||||
# Connection. This includes a default host name, scheme, port, and path.
|
||||
attr_reader :url_prefix
|
||||
|
||||
# @return [Faraday::RackBuilder] Builder for this Connection.
|
||||
attr_reader :builder
|
||||
|
||||
# @return [Hash] SSL options.
|
||||
attr_reader :ssl
|
||||
|
||||
# @return [Object] the parallel manager for this Connection.
|
||||
attr_reader :parallel_manager
|
||||
|
||||
# Sets the default parallel manager for this connection.
|
||||
attr_writer :default_parallel_manager
|
||||
|
||||
# @return [Hash] proxy options.
|
||||
attr_reader :proxy
|
||||
|
||||
# Initializes a new Faraday::Connection.
|
||||
#
|
||||
# @param url [URI, String] URI or String base URL to use as a prefix for all
|
||||
# requests (optional).
|
||||
# @param options [Hash, Faraday::ConnectionOptions]
|
||||
# @option options [URI, String] :url ('http:/') URI or String base URL
|
||||
# @option options [Hash<String => String>] :params URI query unencoded
|
||||
# key/value pairs.
|
||||
# @option options [Hash<String => String>] :headers Hash of unencoded HTTP
|
||||
# header key/value pairs.
|
||||
# @option options [Hash] :request Hash of request options.
|
||||
# @option options [Hash] :ssl Hash of SSL options.
|
||||
# @option options [Hash, URI, String] :proxy proxy options, either as a URL
|
||||
# or as a Hash
|
||||
# @option options [URI, String] :proxy[:uri]
|
||||
# @option options [String] :proxy[:user]
|
||||
# @option options [String] :proxy[:password]
|
||||
# @yield [self] after all setup has been done
|
||||
def initialize(url = nil, options = nil)
|
||||
options = ConnectionOptions.from(options)
|
||||
|
||||
if url.is_a?(Hash) || url.is_a?(ConnectionOptions)
|
||||
options = Utils.deep_merge(options, url)
|
||||
url = options.url
|
||||
end
|
||||
|
||||
@parallel_manager = nil
|
||||
@headers = Utils::Headers.new
|
||||
@params = Utils::ParamsHash.new
|
||||
@options = options.request
|
||||
@ssl = options.ssl
|
||||
@default_parallel_manager = options.parallel_manager
|
||||
@manual_proxy = nil
|
||||
|
||||
@builder = options.builder || begin
|
||||
# pass an empty block to Builder so it doesn't assume default middleware
|
||||
options.new_builder(block_given? ? proc { |b| } : nil)
|
||||
end
|
||||
|
||||
self.url_prefix = url || 'http:/'
|
||||
|
||||
@params.update(options.params) if options.params
|
||||
@headers.update(options.headers) if options.headers
|
||||
|
||||
initialize_proxy(url, options)
|
||||
|
||||
yield(self) if block_given?
|
||||
|
||||
@headers[:user_agent] ||= USER_AGENT
|
||||
end
|
||||
|
||||
def initialize_proxy(url, options)
|
||||
@manual_proxy = !!options.proxy
|
||||
@proxy =
|
||||
if options.proxy
|
||||
ProxyOptions.from(options.proxy)
|
||||
else
|
||||
proxy_from_env(url)
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the Hash of URI query unencoded key/value pairs.
|
||||
# @param hash [Hash]
|
||||
def params=(hash)
|
||||
@params.replace hash
|
||||
end
|
||||
|
||||
# Sets the Hash of unencoded HTTP header key/value pairs.
|
||||
# @param hash [Hash]
|
||||
def headers=(hash)
|
||||
@headers.replace hash
|
||||
end
|
||||
|
||||
extend Forwardable
|
||||
|
||||
def_delegators :builder, :use, :request, :response, :adapter, :app
|
||||
|
||||
# Closes the underlying resources and/or connections. In the case of
|
||||
# persistent connections, this closes all currently open connections
|
||||
# but does not prevent new connections from being made.
|
||||
def close
|
||||
app.close
|
||||
end
|
||||
|
||||
# @!method get(url = nil, params = nil, headers = nil)
|
||||
# Makes a GET HTTP request without a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.get '/items', { page: 1 }, :accept => 'application/json'
|
||||
#
|
||||
# # ElasticSearch example sending a body with GET.
|
||||
# conn.get '/twitter/tweet/_search' do |req|
|
||||
# req.headers[:content_type] = 'application/json'
|
||||
# req.params[:routing] = 'kimchy'
|
||||
# req.body = JSON.generate(query: {...})
|
||||
# end
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!method head(url = nil, params = nil, headers = nil)
|
||||
# Makes a HEAD HTTP request without a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.head '/items/1'
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!method delete(url = nil, params = nil, headers = nil)
|
||||
# Makes a DELETE HTTP request without a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.delete '/items/1'
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!method trace(url = nil, params = nil, headers = nil)
|
||||
# Makes a TRACE HTTP request without a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.connect '/items/1'
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!visibility private
|
||||
METHODS_WITH_QUERY.each do |method|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{method}(url = nil, params = nil, headers = nil)
|
||||
run_request(:#{method}, url, nil, headers) do |request|
|
||||
request.params.update(params) if params
|
||||
yield request if block_given?
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
# @overload options()
|
||||
# Returns current Connection options.
|
||||
#
|
||||
# @overload options(url, params = nil, headers = nil)
|
||||
# Makes an OPTIONS HTTP request to the given URL.
|
||||
# @param url [String, URI, nil] String base URL to sue as a prefix for all requests.
|
||||
# @param params [Hash, nil] Hash of URI query unencoded key/value pairs.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.options '/items/1'
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
def options(*args)
|
||||
return @options if args.empty?
|
||||
|
||||
url, params, headers = *args
|
||||
run_request(:options, url, nil, headers) do |request|
|
||||
request.params.update(params) if params
|
||||
yield request if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
# @!method post(url = nil, body = nil, headers = nil)
|
||||
# Makes a POST HTTP request with a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param body [String, nil] body for the request.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.post '/items', data, content_type: 'application/json'
|
||||
#
|
||||
# # Simple ElasticSearch indexing sample.
|
||||
# conn.post '/twitter/tweet' do |req|
|
||||
# req.headers[:content_type] = 'application/json'
|
||||
# req.params[:routing] = 'kimchy'
|
||||
# req.body = JSON.generate(user: 'kimchy', ...)
|
||||
# end
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!method put(url = nil, body = nil, headers = nil)
|
||||
# Makes a PUT HTTP request with a body.
|
||||
# @!scope class
|
||||
#
|
||||
# @param url [String, URI, nil] The optional String base URL to use as a prefix for
|
||||
# all requests. Can also be the options Hash.
|
||||
# @param body [String, nil] body for the request.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @example
|
||||
# conn.put '/products/123', data, content_type: 'application/json'
|
||||
#
|
||||
# # Star a gist.
|
||||
# conn.put 'https://api.github.com/gists/GIST_ID/star' do |req|
|
||||
# req.headers['Accept'] = 'application/vnd.github+json'
|
||||
# req.headers['Authorization'] = 'Bearer <YOUR-TOKEN>'
|
||||
# req.headers['X-GitHub-Api-Version'] = '2022-11-28'
|
||||
# end
|
||||
#
|
||||
# @yield [Faraday::Request] for further request customizations
|
||||
# @return [Faraday::Response]
|
||||
|
||||
# @!visibility private
|
||||
METHODS_WITH_BODY.each do |method|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{method}(url = nil, body = nil, headers = nil, &block)
|
||||
run_request(:#{method}, url, body, headers, &block)
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
# Check if the adapter is parallel-capable.
|
||||
#
|
||||
# @yield if the adapter isn't parallel-capable, or if no adapter is set yet.
|
||||
#
|
||||
# @return [Object, nil] a parallel manager or nil if yielded
|
||||
# @api private
|
||||
def default_parallel_manager
|
||||
@default_parallel_manager ||= begin
|
||||
adapter = @builder.adapter.klass if @builder.adapter
|
||||
|
||||
if support_parallel?(adapter)
|
||||
adapter.setup_parallel_manager
|
||||
elsif block_given?
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Determine if this Faraday::Connection can make parallel requests.
|
||||
#
|
||||
# @return [Boolean]
|
||||
def in_parallel?
|
||||
!!@parallel_manager
|
||||
end
|
||||
|
||||
# Sets up the parallel manager to make a set of requests.
|
||||
#
|
||||
# @param manager [Object] The parallel manager that this Connection's
|
||||
# Adapter uses.
|
||||
#
|
||||
# @yield a block to execute multiple requests.
|
||||
# @return [void]
|
||||
def in_parallel(manager = nil)
|
||||
@parallel_manager = manager || default_parallel_manager do
|
||||
warn 'Warning: `in_parallel` called but no parallel-capable adapter ' \
|
||||
'on Faraday stack'
|
||||
warn caller[2, 10].join("\n")
|
||||
nil
|
||||
end
|
||||
yield
|
||||
@parallel_manager&.run
|
||||
ensure
|
||||
@parallel_manager = nil
|
||||
end
|
||||
|
||||
# Sets the Hash proxy options.
|
||||
#
|
||||
# @param new_value [Object]
|
||||
def proxy=(new_value)
|
||||
@manual_proxy = true
|
||||
@proxy = new_value ? ProxyOptions.from(new_value) : nil
|
||||
end
|
||||
|
||||
def_delegators :url_prefix, :scheme, :scheme=, :host, :host=, :port, :port=
|
||||
def_delegator :url_prefix, :path, :path_prefix
|
||||
|
||||
# Parses the given URL with URI and stores the individual
|
||||
# components in this connection. These components serve as defaults for
|
||||
# requests made by this connection.
|
||||
#
|
||||
# @param url [String, URI]
|
||||
# @param encoder [Object]
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# conn = Faraday::Connection.new { ... }
|
||||
# conn.url_prefix = "https://httpbingo.org/api"
|
||||
# conn.scheme # => https
|
||||
# conn.path_prefix # => "/api"
|
||||
#
|
||||
# conn.get("nigiri?page=2") # accesses https://httpbingo.org/api/nigiri
|
||||
def url_prefix=(url, encoder = nil)
|
||||
uri = @url_prefix = Utils.URI(url)
|
||||
self.path_prefix = uri.path
|
||||
|
||||
params.merge_query(uri.query, encoder)
|
||||
uri.query = nil
|
||||
|
||||
with_uri_credentials(uri) do |user, password|
|
||||
set_basic_auth(user, password)
|
||||
uri.user = uri.password = nil
|
||||
end
|
||||
|
||||
@proxy = proxy_from_env(url) unless @manual_proxy
|
||||
end
|
||||
|
||||
def set_basic_auth(user, password)
|
||||
header = Faraday::Utils.basic_header_from(user, password)
|
||||
headers[Faraday::Request::Authorization::KEY] = header
|
||||
end
|
||||
|
||||
# Sets the path prefix and ensures that it always has a leading
|
||||
# slash.
|
||||
#
|
||||
# @param value [String]
|
||||
#
|
||||
# @return [String] the new path prefix
|
||||
def path_prefix=(value)
|
||||
url_prefix.path = if value
|
||||
value = "/#{value}" unless value[0, 1] == '/'
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
# Takes a relative url for a request and combines it with the defaults
|
||||
# set on the connection instance.
|
||||
#
|
||||
# @param url [String, URI, nil]
|
||||
# @param extra_params [Hash]
|
||||
#
|
||||
# @example
|
||||
# conn = Faraday::Connection.new { ... }
|
||||
# conn.url_prefix = "https://httpbingo.org/api?token=abc"
|
||||
# conn.scheme # => https
|
||||
# conn.path_prefix # => "/api"
|
||||
#
|
||||
# conn.build_url("nigiri?page=2")
|
||||
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
|
||||
#
|
||||
# conn.build_url("nigiri", page: 2)
|
||||
# # => https://httpbingo.org/api/nigiri?token=abc&page=2
|
||||
#
|
||||
def build_url(url = nil, extra_params = nil)
|
||||
uri = build_exclusive_url(url)
|
||||
|
||||
query_values = params.dup.merge_query(uri.query, options.params_encoder)
|
||||
query_values.update(extra_params) if extra_params
|
||||
uri.query =
|
||||
if query_values.empty?
|
||||
nil
|
||||
else
|
||||
query_values.to_query(options.params_encoder)
|
||||
end
|
||||
|
||||
uri
|
||||
end
|
||||
|
||||
# Builds and runs the Faraday::Request.
|
||||
#
|
||||
# @param method [Symbol] HTTP method.
|
||||
# @param url [String, URI, nil] String or URI to access.
|
||||
# @param body [String, nil] The request body that will eventually be converted to
|
||||
# a string.
|
||||
# @param headers [Hash, nil] unencoded HTTP header key/value pairs.
|
||||
#
|
||||
# @return [Faraday::Response]
|
||||
def run_request(method, url, body, headers)
|
||||
unless METHODS.include?(method)
|
||||
raise ArgumentError, "unknown http method: #{method}"
|
||||
end
|
||||
|
||||
request = build_request(method) do |req|
|
||||
req.options.proxy = proxy_for_request(url)
|
||||
req.url(url) if url
|
||||
req.headers.update(headers) if headers
|
||||
req.body = body if body
|
||||
yield(req) if block_given?
|
||||
end
|
||||
|
||||
builder.build_response(self, request)
|
||||
end
|
||||
|
||||
# Creates and configures the request object.
|
||||
#
|
||||
# @param method [Symbol]
|
||||
#
|
||||
# @yield [Faraday::Request] if block given
|
||||
# @return [Faraday::Request]
|
||||
def build_request(method)
|
||||
Request.create(method) do |req|
|
||||
req.params = params.dup
|
||||
req.headers = headers.dup
|
||||
req.options = options.dup
|
||||
yield(req) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
# Build an absolute URL based on url_prefix.
|
||||
#
|
||||
# @param url [String, URI, nil]
|
||||
# @param params [Faraday::Utils::ParamsHash] A Faraday::Utils::ParamsHash to
|
||||
# replace the query values
|
||||
# of the resulting url (default: nil).
|
||||
#
|
||||
# @return [URI]
|
||||
def build_exclusive_url(url = nil, params = nil, params_encoder = nil)
|
||||
url = nil if url.respond_to?(:empty?) && url.empty?
|
||||
base = url_prefix.dup
|
||||
if url && !base.path.end_with?('/')
|
||||
base.path = "#{base.path}/" # ensure trailing slash
|
||||
end
|
||||
url = url.to_s.gsub(':', '%3A') if URI.parse(url.to_s).opaque
|
||||
uri = url ? base + url : base
|
||||
if params
|
||||
uri.query = params.to_query(params_encoder || options.params_encoder)
|
||||
end
|
||||
uri.query = nil if uri.query && uri.query.empty?
|
||||
uri
|
||||
end
|
||||
|
||||
# Creates a duplicate of this Faraday::Connection.
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
# @return [Faraday::Connection]
|
||||
def dup
|
||||
self.class.new(build_exclusive_url,
|
||||
headers: headers.dup,
|
||||
params: params.dup,
|
||||
builder: builder.dup,
|
||||
ssl: ssl.dup,
|
||||
request: options.dup)
|
||||
end
|
||||
|
||||
# Yields username and password extracted from a URI if they both exist.
|
||||
#
|
||||
# @param uri [URI]
|
||||
# @yield [username, password] any username and password
|
||||
# @yieldparam username [String] any username from URI
|
||||
# @yieldparam password [String] any password from URI
|
||||
# @return [void]
|
||||
# @api private
|
||||
def with_uri_credentials(uri)
|
||||
return unless uri.user && uri.password
|
||||
|
||||
yield(Utils.unescape(uri.user), Utils.unescape(uri.password))
|
||||
end
|
||||
|
||||
def proxy_from_env(url)
|
||||
return if Faraday.ignore_env_proxy
|
||||
|
||||
uri = nil
|
||||
case url
|
||||
when String
|
||||
uri = Utils.URI(url)
|
||||
uri = if uri.host.nil?
|
||||
find_default_proxy
|
||||
else
|
||||
URI.parse("#{uri.scheme}://#{uri.host}").find_proxy
|
||||
end
|
||||
when URI
|
||||
uri = url.find_proxy
|
||||
when nil
|
||||
uri = find_default_proxy
|
||||
end
|
||||
ProxyOptions.from(uri) if uri
|
||||
end
|
||||
|
||||
def find_default_proxy
|
||||
uri = ENV.fetch('http_proxy', nil)
|
||||
return unless uri && !uri.empty?
|
||||
|
||||
uri = "http://#{uri}" unless uri.match?(/^http/i)
|
||||
uri
|
||||
end
|
||||
|
||||
def proxy_for_request(url)
|
||||
return proxy if @manual_proxy
|
||||
|
||||
if url && Utils.URI(url).absolute?
|
||||
proxy_from_env(url)
|
||||
else
|
||||
proxy
|
||||
end
|
||||
end
|
||||
|
||||
def support_parallel?(adapter)
|
||||
adapter.respond_to?(:supports_parallel?) && adapter&.supports_parallel?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,105 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# FlatParamsEncoder manages URI params as a flat hash. Any Array values repeat
|
||||
# the parameter multiple times.
|
||||
module FlatParamsEncoder
|
||||
class << self
|
||||
extend Forwardable
|
||||
def_delegators :'Faraday::Utils', :escape, :unescape
|
||||
end
|
||||
|
||||
# Encode converts the given param into a URI querystring. Keys and values
|
||||
# will converted to strings and appropriately escaped for the URI.
|
||||
#
|
||||
# @param params [Hash] query arguments to convert.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# encode({a: %w[one two three], b: true, c: "C"})
|
||||
# # => 'a=one&a=two&a=three&b=true&c=C'
|
||||
#
|
||||
# @return [String] the URI querystring (without the leading '?')
|
||||
def self.encode(params)
|
||||
return nil if params.nil?
|
||||
|
||||
unless params.is_a?(Array)
|
||||
unless params.respond_to?(:to_hash)
|
||||
raise TypeError,
|
||||
"Can't convert #{params.class} into Hash."
|
||||
end
|
||||
params = params.to_hash
|
||||
params = params.map do |key, value|
|
||||
key = key.to_s if key.is_a?(Symbol)
|
||||
[key, value]
|
||||
end
|
||||
|
||||
# Only to be used for non-Array inputs. Arrays should preserve order.
|
||||
params.sort! if @sort_params
|
||||
end
|
||||
|
||||
# The params have form [['key1', 'value1'], ['key2', 'value2']].
|
||||
buffer = +''
|
||||
params.each do |key, value|
|
||||
encoded_key = escape(key)
|
||||
if value.nil?
|
||||
buffer << "#{encoded_key}&"
|
||||
elsif value.is_a?(Array)
|
||||
if value.empty?
|
||||
buffer << "#{encoded_key}=&"
|
||||
else
|
||||
value.each do |sub_value|
|
||||
encoded_value = escape(sub_value)
|
||||
buffer << "#{encoded_key}=#{encoded_value}&"
|
||||
end
|
||||
end
|
||||
else
|
||||
encoded_value = escape(value)
|
||||
buffer << "#{encoded_key}=#{encoded_value}&"
|
||||
end
|
||||
end
|
||||
buffer.chop
|
||||
end
|
||||
|
||||
# Decode converts the given URI querystring into a hash.
|
||||
#
|
||||
# @param query [String] query arguments to parse.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# decode('a=one&a=two&a=three&b=true&c=C')
|
||||
# # => {"a"=>["one", "two", "three"], "b"=>"true", "c"=>"C"}
|
||||
#
|
||||
# @return [Hash] parsed keys and value strings from the querystring.
|
||||
def self.decode(query)
|
||||
return nil if query.nil?
|
||||
|
||||
empty_accumulator = {}
|
||||
|
||||
split_query = (query.split('&').map do |pair|
|
||||
pair.split('=', 2) if pair && !pair.empty?
|
||||
end).compact
|
||||
split_query.each_with_object(empty_accumulator.dup) do |pair, accu|
|
||||
pair[0] = unescape(pair[0])
|
||||
pair[1] = true if pair[1].nil?
|
||||
if pair[1].respond_to?(:to_str)
|
||||
pair[1] = unescape(pair[1].to_str.tr('+', ' '))
|
||||
end
|
||||
if accu[pair[0]].is_a?(Array)
|
||||
accu[pair[0]] << pair[1]
|
||||
elsif accu[pair[0]]
|
||||
accu[pair[0]] = [accu[pair[0]], pair[1]]
|
||||
else
|
||||
accu[pair[0]] = pair[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_accessor :sort_params
|
||||
end
|
||||
|
||||
# Useful default for OAuth and caching.
|
||||
@sort_params = true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,183 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Sub-module for encoding parameters into query-string.
|
||||
module EncodeMethods
|
||||
# @param params [nil, Array, #to_hash] parameters to be encoded
|
||||
#
|
||||
# @return [String] the encoded params
|
||||
#
|
||||
# @raise [TypeError] if params can not be converted to a Hash
|
||||
def encode(params)
|
||||
return nil if params.nil?
|
||||
|
||||
unless params.is_a?(Array)
|
||||
unless params.respond_to?(:to_hash)
|
||||
raise TypeError, "Can't convert #{params.class} into Hash."
|
||||
end
|
||||
|
||||
params = params.to_hash
|
||||
params = params.map do |key, value|
|
||||
key = key.to_s if key.is_a?(Symbol)
|
||||
[key, value]
|
||||
end
|
||||
|
||||
# Only to be used for non-Array inputs. Arrays should preserve order.
|
||||
params.sort! if @sort_params
|
||||
end
|
||||
|
||||
# The params have form [['key1', 'value1'], ['key2', 'value2']].
|
||||
buffer = +''
|
||||
params.each do |parent, value|
|
||||
encoded_parent = escape(parent)
|
||||
buffer << "#{encode_pair(encoded_parent, value)}&"
|
||||
end
|
||||
buffer.chop
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def encode_pair(parent, value)
|
||||
if value.is_a?(Hash)
|
||||
encode_hash(parent, value)
|
||||
elsif value.is_a?(Array)
|
||||
encode_array(parent, value)
|
||||
elsif value.nil?
|
||||
parent
|
||||
else
|
||||
encoded_value = escape(value)
|
||||
"#{parent}=#{encoded_value}"
|
||||
end
|
||||
end
|
||||
|
||||
def encode_hash(parent, value)
|
||||
value = value.map { |key, val| [escape(key), val] }.sort
|
||||
|
||||
buffer = +''
|
||||
value.each do |key, val|
|
||||
new_parent = "#{parent}%5B#{key}%5D"
|
||||
buffer << "#{encode_pair(new_parent, val)}&"
|
||||
end
|
||||
buffer.chop
|
||||
end
|
||||
|
||||
def encode_array(parent, value)
|
||||
return "#{parent}%5B%5D" if value.empty?
|
||||
|
||||
buffer = +''
|
||||
value.each_with_index do |val, index|
|
||||
new_parent = if @array_indices
|
||||
"#{parent}%5B#{index}%5D"
|
||||
else
|
||||
"#{parent}%5B%5D"
|
||||
end
|
||||
buffer << "#{encode_pair(new_parent, val)}&"
|
||||
end
|
||||
buffer.chop
|
||||
end
|
||||
end
|
||||
|
||||
# Sub-module for decoding query-string into parameters.
|
||||
module DecodeMethods
|
||||
# @param query [nil, String]
|
||||
#
|
||||
# @return [Array<Array, String>] the decoded params
|
||||
#
|
||||
# @raise [TypeError] if the nesting is incorrect
|
||||
def decode(query)
|
||||
return nil if query.nil?
|
||||
|
||||
params = {}
|
||||
query.split('&').each do |pair|
|
||||
next if pair.empty?
|
||||
|
||||
key, value = pair.split('=', 2)
|
||||
key = unescape(key)
|
||||
value = unescape(value.tr('+', ' ')) if value
|
||||
decode_pair(key, value, params)
|
||||
end
|
||||
|
||||
dehash(params, 0)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
SUBKEYS_REGEX = /[^\[\]]+(?:\]?\[\])?/
|
||||
|
||||
def decode_pair(key, value, context)
|
||||
subkeys = key.scan(SUBKEYS_REGEX)
|
||||
subkeys.each_with_index do |subkey, i|
|
||||
is_array = subkey =~ /[\[\]]+\Z/
|
||||
subkey = Regexp.last_match.pre_match if is_array
|
||||
last_subkey = i == subkeys.length - 1
|
||||
|
||||
context = prepare_context(context, subkey, is_array, last_subkey)
|
||||
add_to_context(is_array, context, value, subkey) if last_subkey
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_context(context, subkey, is_array, last_subkey)
|
||||
if !last_subkey || is_array
|
||||
context = new_context(subkey, is_array, context)
|
||||
end
|
||||
if context.is_a?(Array) && !is_array
|
||||
context = match_context(context, subkey)
|
||||
end
|
||||
context
|
||||
end
|
||||
|
||||
def new_context(subkey, is_array, context)
|
||||
value_type = is_array ? Array : Hash
|
||||
if context[subkey] && !context[subkey].is_a?(value_type)
|
||||
raise TypeError, "expected #{value_type.name} " \
|
||||
"(got #{context[subkey].class.name}) for param `#{subkey}'"
|
||||
end
|
||||
|
||||
context[subkey] ||= value_type.new
|
||||
end
|
||||
|
||||
def match_context(context, subkey)
|
||||
context << {} if !context.last.is_a?(Hash) || context.last.key?(subkey)
|
||||
context.last
|
||||
end
|
||||
|
||||
def add_to_context(is_array, context, value, subkey)
|
||||
is_array ? context << value : context[subkey] = value
|
||||
end
|
||||
|
||||
# Internal: convert a nested hash with purely numeric keys into an array.
|
||||
# FIXME: this is not compatible with Rack::Utils.parse_nested_query
|
||||
# @!visibility private
|
||||
def dehash(hash, depth)
|
||||
hash.each do |key, value|
|
||||
hash[key] = dehash(value, depth + 1) if value.is_a?(Hash)
|
||||
end
|
||||
|
||||
if depth.positive? && !hash.empty? && hash.keys.all? { |k| k =~ /^\d+$/ }
|
||||
hash.sort.map(&:last)
|
||||
else
|
||||
hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This is the default encoder for Faraday requests.
|
||||
# Using this encoder, parameters will be encoded respecting their structure,
|
||||
# so you can send objects such as Arrays or Hashes as parameters
|
||||
# for your requests.
|
||||
module NestedParamsEncoder
|
||||
class << self
|
||||
attr_accessor :sort_params, :array_indices
|
||||
|
||||
extend Forwardable
|
||||
def_delegators :'Faraday::Utils', :escape, :unescape
|
||||
end
|
||||
|
||||
# Useful default for OAuth and caching.
|
||||
@sort_params = true
|
||||
@array_indices = false
|
||||
|
||||
extend EncodeMethods
|
||||
extend DecodeMethods
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,161 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Faraday namespace.
|
||||
module Faraday
|
||||
# Faraday error base class.
|
||||
class Error < StandardError
|
||||
attr_reader :response, :wrapped_exception
|
||||
|
||||
def initialize(exc = nil, response = nil)
|
||||
@wrapped_exception = nil unless defined?(@wrapped_exception)
|
||||
@response = nil unless defined?(@response)
|
||||
super(exc_msg_and_response!(exc, response))
|
||||
end
|
||||
|
||||
def backtrace
|
||||
if @wrapped_exception
|
||||
@wrapped_exception.backtrace
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
inner = +''
|
||||
inner << " wrapped=#{@wrapped_exception.inspect}" if @wrapped_exception
|
||||
inner << " response=#{@response.inspect}" if @response
|
||||
inner << " #{super}" if inner.empty?
|
||||
%(#<#{self.class}#{inner}>)
|
||||
end
|
||||
|
||||
def response_status
|
||||
return unless @response
|
||||
|
||||
@response.is_a?(Faraday::Response) ? @response.status : @response[:status]
|
||||
end
|
||||
|
||||
def response_headers
|
||||
return unless @response
|
||||
|
||||
@response.is_a?(Faraday::Response) ? @response.headers : @response[:headers]
|
||||
end
|
||||
|
||||
def response_body
|
||||
return unless @response
|
||||
|
||||
@response.is_a?(Faraday::Response) ? @response.body : @response[:body]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Pulls out potential parent exception and response hash, storing them in
|
||||
# instance variables.
|
||||
# exc - Either an Exception, a string message, or a response hash.
|
||||
# response - Hash
|
||||
# :status - Optional integer HTTP response status
|
||||
# :headers - String key/value hash of HTTP response header
|
||||
# values.
|
||||
# :body - Optional string HTTP response body.
|
||||
# :request - Hash
|
||||
# :method - Symbol with the request HTTP method.
|
||||
# :url - URI object with the url requested.
|
||||
# :url_path - String with the url path requested.
|
||||
# :params - String key/value hash of query params
|
||||
# present in the request.
|
||||
# :headers - String key/value hash of HTTP request
|
||||
# header values.
|
||||
# :body - String HTTP request body.
|
||||
#
|
||||
# If a subclass has to call this, then it should pass a string message
|
||||
# to `super`. See NilStatusError.
|
||||
def exc_msg_and_response!(exc, response = nil)
|
||||
if @response.nil? && @wrapped_exception.nil?
|
||||
@wrapped_exception, msg, @response = exc_msg_and_response(exc, response)
|
||||
return msg
|
||||
end
|
||||
|
||||
exc.to_s
|
||||
end
|
||||
|
||||
# Pulls out potential parent exception and response hash.
|
||||
def exc_msg_and_response(exc, response = nil)
|
||||
return [exc, exc.message, response] if exc.respond_to?(:backtrace)
|
||||
|
||||
return [nil, "the server responded with status #{exc[:status]}", exc] \
|
||||
if exc.respond_to?(:each_key)
|
||||
|
||||
[nil, exc.to_s, response]
|
||||
end
|
||||
end
|
||||
|
||||
# Faraday client error class. Represents 4xx status responses.
|
||||
class ClientError < Error
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 400 response.
|
||||
class BadRequestError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 401 response.
|
||||
class UnauthorizedError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 403 response.
|
||||
class ForbiddenError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 404 response.
|
||||
class ResourceNotFound < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 407 response.
|
||||
class ProxyAuthError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 408 response.
|
||||
class RequestTimeoutError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 409 response.
|
||||
class ConflictError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 422 response.
|
||||
class UnprocessableEntityError < ClientError
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a 429 response.
|
||||
class TooManyRequestsError < ClientError
|
||||
end
|
||||
|
||||
# Faraday server error class. Represents 5xx status responses.
|
||||
class ServerError < Error
|
||||
end
|
||||
|
||||
# A unified client error for timeouts.
|
||||
class TimeoutError < ServerError
|
||||
def initialize(exc = 'timeout', response = nil)
|
||||
super(exc, response)
|
||||
end
|
||||
end
|
||||
|
||||
# Raised by Faraday::Response::RaiseError in case of a nil status in response.
|
||||
class NilStatusError < ServerError
|
||||
def initialize(exc, response = nil)
|
||||
exc_msg_and_response!(exc, response)
|
||||
super('http status could not be derived from the server response')
|
||||
end
|
||||
end
|
||||
|
||||
# A unified error for failed connections.
|
||||
class ConnectionFailed < Error
|
||||
end
|
||||
|
||||
# A unified client error for SSL errors.
|
||||
class SSLError < Error
|
||||
end
|
||||
|
||||
# Raised by middlewares that parse the response, like the JSON response middleware.
|
||||
class ParsingError < Error
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'pp' # This require is necessary for Hash#pretty_inspect to work, do not remove it, people rely on it.
|
||||
|
||||
module Faraday
|
||||
module Logging
|
||||
# Serves as an integration point to customize logging
|
||||
class Formatter
|
||||
extend Forwardable
|
||||
|
||||
DEFAULT_OPTIONS = { headers: true, bodies: false, errors: false,
|
||||
log_level: :info }.freeze
|
||||
|
||||
def initialize(logger:, options:)
|
||||
@logger = logger
|
||||
@options = DEFAULT_OPTIONS.merge(options)
|
||||
unless %i[debug info warn error fatal].include?(@options[:log_level])
|
||||
@options[:log_level] = :info
|
||||
end
|
||||
@filter = []
|
||||
end
|
||||
|
||||
def_delegators :@logger, :debug, :info, :warn, :error, :fatal
|
||||
|
||||
def request(env)
|
||||
public_send(log_level, 'request') do
|
||||
"#{env.method.upcase} #{apply_filters(env.url.to_s)}"
|
||||
end
|
||||
|
||||
log_headers('request', env.request_headers) if log_headers?(:request)
|
||||
log_body('request', env[:body]) if env[:body] && log_body?(:request)
|
||||
end
|
||||
|
||||
def response(env)
|
||||
public_send(log_level, 'response') { "Status #{env.status}" }
|
||||
|
||||
log_headers('response', env.response_headers) if log_headers?(:response)
|
||||
log_body('response', env[:body]) if env[:body] && log_body?(:response)
|
||||
end
|
||||
|
||||
def exception(exc)
|
||||
return unless log_errors?
|
||||
|
||||
public_send(log_level, 'error') { exc.full_message }
|
||||
|
||||
log_headers('error', exc.response_headers) if exc.respond_to?(:response_headers) && log_headers?(:error)
|
||||
return unless exc.respond_to?(:response_body) && exc.response_body && log_body?(:error)
|
||||
|
||||
log_body('error', exc.response_body)
|
||||
end
|
||||
|
||||
def filter(filter_word, filter_replacement)
|
||||
@filter.push([filter_word, filter_replacement])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dump_headers(headers)
|
||||
return if headers.nil?
|
||||
|
||||
headers.map { |k, v| "#{k}: #{v.inspect}" }.join("\n")
|
||||
end
|
||||
|
||||
def dump_body(body)
|
||||
if body.respond_to?(:to_str)
|
||||
body.to_str
|
||||
else
|
||||
pretty_inspect(body)
|
||||
end
|
||||
end
|
||||
|
||||
def pretty_inspect(body)
|
||||
body.pretty_inspect
|
||||
end
|
||||
|
||||
def log_headers?(type)
|
||||
case @options[:headers]
|
||||
when Hash
|
||||
@options[:headers][type]
|
||||
else
|
||||
@options[:headers]
|
||||
end
|
||||
end
|
||||
|
||||
def log_body?(type)
|
||||
case @options[:bodies]
|
||||
when Hash
|
||||
@options[:bodies][type]
|
||||
else
|
||||
@options[:bodies]
|
||||
end
|
||||
end
|
||||
|
||||
def log_errors?
|
||||
@options[:errors]
|
||||
end
|
||||
|
||||
def apply_filters(output)
|
||||
@filter.each do |pattern, replacement|
|
||||
output = output.to_s.gsub(pattern, replacement)
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
def log_level
|
||||
@options[:log_level]
|
||||
end
|
||||
|
||||
def log_headers(type, headers)
|
||||
public_send(log_level, type) { apply_filters(dump_headers(headers)) }
|
||||
end
|
||||
|
||||
def log_body(type, body)
|
||||
public_send(log_level, type) { apply_filters(dump_body(body)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
METHODS_WITH_QUERY = %w[get head delete trace].freeze
|
||||
METHODS_WITH_BODY = %w[post put patch].freeze
|
||||
end
|
||||
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Middleware is the basic base class of any Faraday middleware.
|
||||
class Middleware
|
||||
extend MiddlewareRegistry
|
||||
|
||||
attr_reader :app, :options
|
||||
|
||||
def initialize(app = nil, options = {})
|
||||
@app = app
|
||||
@options = options
|
||||
end
|
||||
|
||||
def call(env)
|
||||
on_request(env) if respond_to?(:on_request)
|
||||
app.call(env).on_complete do |environment|
|
||||
on_complete(environment) if respond_to?(:on_complete)
|
||||
end
|
||||
rescue StandardError => e
|
||||
on_error(e) if respond_to?(:on_error)
|
||||
raise
|
||||
end
|
||||
|
||||
def close
|
||||
if app.respond_to?(:close)
|
||||
app.close
|
||||
else
|
||||
warn "#{app} does not implement \#close!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'monitor'
|
||||
|
||||
module Faraday
|
||||
# Adds the ability for other modules to register and lookup
|
||||
# middleware classes.
|
||||
module MiddlewareRegistry
|
||||
def registered_middleware
|
||||
@registered_middleware ||= {}
|
||||
end
|
||||
|
||||
# Register middleware class(es) on the current module.
|
||||
#
|
||||
# @param mappings [Hash] Middleware mappings from a lookup symbol to a middleware class.
|
||||
# @return [void]
|
||||
#
|
||||
# @example Lookup by a constant
|
||||
#
|
||||
# module Faraday
|
||||
# class Whatever < Middleware
|
||||
# # Middleware looked up by :foo returns Faraday::Whatever::Foo.
|
||||
# register_middleware(foo: Whatever)
|
||||
# end
|
||||
# end
|
||||
def register_middleware(**mappings)
|
||||
middleware_mutex do
|
||||
registered_middleware.update(mappings)
|
||||
end
|
||||
end
|
||||
|
||||
# Unregister a previously registered middleware class.
|
||||
#
|
||||
# @param key [Symbol] key for the registered middleware.
|
||||
def unregister_middleware(key)
|
||||
registered_middleware.delete(key)
|
||||
end
|
||||
|
||||
# Lookup middleware class with a registered Symbol shortcut.
|
||||
#
|
||||
# @param key [Symbol] key for the registered middleware.
|
||||
# @return [Class] a middleware Class.
|
||||
# @raise [Faraday::Error] if given key is not registered
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# module Faraday
|
||||
# class Whatever < Middleware
|
||||
# register_middleware(foo: Whatever)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Faraday::Middleware.lookup_middleware(:foo)
|
||||
# # => Faraday::Whatever
|
||||
def lookup_middleware(key)
|
||||
load_middleware(key) ||
|
||||
raise(Faraday::Error, "#{key.inspect} is not registered on #{self}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def middleware_mutex(&block)
|
||||
@middleware_mutex ||= Monitor.new
|
||||
@middleware_mutex.synchronize(&block)
|
||||
end
|
||||
|
||||
def load_middleware(key)
|
||||
value = registered_middleware[key]
|
||||
case value
|
||||
when Module
|
||||
value
|
||||
when Symbol, String
|
||||
middleware_mutex do
|
||||
@registered_middleware[key] = const_get(value)
|
||||
end
|
||||
when Proc
|
||||
middleware_mutex do
|
||||
@registered_middleware[key] = value.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,219 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Subclasses Struct with some special helpers for converting from a Hash to
|
||||
# a Struct.
|
||||
class Options < Struct
|
||||
# Public
|
||||
def self.from(value)
|
||||
value ? new.update(value) : new
|
||||
end
|
||||
|
||||
# Public
|
||||
def each
|
||||
return to_enum(:each) unless block_given?
|
||||
|
||||
members.each do |key|
|
||||
yield(key.to_sym, send(key))
|
||||
end
|
||||
end
|
||||
|
||||
# Public
|
||||
def update(obj)
|
||||
obj.each do |key, value|
|
||||
sub_options = self.class.options_for(key)
|
||||
if sub_options
|
||||
new_value = sub_options.from(value) if value
|
||||
elsif value.is_a?(Hash)
|
||||
new_value = value.dup
|
||||
else
|
||||
new_value = value
|
||||
end
|
||||
|
||||
send(:"#{key}=", new_value) unless new_value.nil?
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Public
|
||||
def delete(key)
|
||||
value = send(key)
|
||||
send(:"#{key}=", nil)
|
||||
value
|
||||
end
|
||||
|
||||
# Public
|
||||
def clear
|
||||
members.each { |member| delete(member) }
|
||||
end
|
||||
|
||||
# Public
|
||||
def merge!(other)
|
||||
other.each do |key, other_value|
|
||||
self_value = send(key)
|
||||
sub_options = self.class.options_for(key)
|
||||
new_value = if self_value && sub_options && other_value
|
||||
self_value.merge(other_value)
|
||||
else
|
||||
other_value
|
||||
end
|
||||
send(:"#{key}=", new_value) unless new_value.nil?
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Public
|
||||
def merge(other)
|
||||
dup.merge!(other)
|
||||
end
|
||||
|
||||
# Public
|
||||
def deep_dup
|
||||
self.class.from(self)
|
||||
end
|
||||
|
||||
# Public
|
||||
def fetch(key, *args)
|
||||
unless symbolized_key_set.include?(key.to_sym)
|
||||
key_setter = "#{key}="
|
||||
if !args.empty?
|
||||
send(key_setter, args.first)
|
||||
elsif block_given?
|
||||
send(key_setter, yield(key))
|
||||
else
|
||||
raise self.class.fetch_error_class, "key not found: #{key.inspect}"
|
||||
end
|
||||
end
|
||||
send(key)
|
||||
end
|
||||
|
||||
# Public
|
||||
def values_at(*keys)
|
||||
keys.map { |key| send(key) }
|
||||
end
|
||||
|
||||
# Public
|
||||
def keys
|
||||
members.reject { |member| send(member).nil? }
|
||||
end
|
||||
|
||||
# Public
|
||||
def empty?
|
||||
keys.empty?
|
||||
end
|
||||
|
||||
# Public
|
||||
def each_key(&block)
|
||||
return to_enum(:each_key) unless block
|
||||
|
||||
keys.each(&block)
|
||||
end
|
||||
|
||||
# Public
|
||||
def key?(key)
|
||||
keys.include?(key)
|
||||
end
|
||||
|
||||
alias has_key? key?
|
||||
|
||||
# Public
|
||||
def each_value(&block)
|
||||
return to_enum(:each_value) unless block
|
||||
|
||||
values.each(&block)
|
||||
end
|
||||
|
||||
# Public
|
||||
def value?(value)
|
||||
values.include?(value)
|
||||
end
|
||||
|
||||
alias has_value? value?
|
||||
|
||||
# Public
|
||||
def to_hash
|
||||
hash = {}
|
||||
members.each do |key|
|
||||
value = send(key)
|
||||
hash[key.to_sym] = value unless value.nil?
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
# Internal
|
||||
def inspect
|
||||
values = []
|
||||
members.each do |member|
|
||||
value = send(member)
|
||||
values << "#{member}=#{value.inspect}" if value
|
||||
end
|
||||
values = values.empty? ? '(empty)' : values.join(', ')
|
||||
|
||||
%(#<#{self.class} #{values}>)
|
||||
end
|
||||
|
||||
# Internal
|
||||
def self.options(mapping)
|
||||
attribute_options.update(mapping)
|
||||
end
|
||||
|
||||
# Internal
|
||||
def self.options_for(key)
|
||||
attribute_options[key]
|
||||
end
|
||||
|
||||
# Internal
|
||||
def self.attribute_options
|
||||
@attribute_options ||= {}
|
||||
end
|
||||
|
||||
def self.memoized(key, &block)
|
||||
unless block
|
||||
raise ArgumentError, '#memoized must be called with a block'
|
||||
end
|
||||
|
||||
memoized_attributes[key.to_sym] = block
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
remove_method(key) if method_defined?(key, false)
|
||||
def #{key}() self[:#{key}]; end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def self.memoized_attributes
|
||||
@memoized_attributes ||= {}
|
||||
end
|
||||
|
||||
def [](key)
|
||||
key = key.to_sym
|
||||
if (method = self.class.memoized_attributes[key])
|
||||
super(key) || (self[key] = instance_eval(&method))
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def symbolized_key_set
|
||||
@symbolized_key_set ||= Set.new(keys.map(&:to_sym))
|
||||
end
|
||||
|
||||
def self.inherited(subclass)
|
||||
super
|
||||
subclass.attribute_options.update(attribute_options)
|
||||
subclass.memoized_attributes.update(memoized_attributes)
|
||||
end
|
||||
|
||||
def self.fetch_error_class
|
||||
@fetch_error_class ||= if Object.const_defined?(:KeyError)
|
||||
::KeyError
|
||||
else
|
||||
::IndexError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'faraday/options/request_options'
|
||||
require 'faraday/options/ssl_options'
|
||||
require 'faraday/options/proxy_options'
|
||||
require 'faraday/options/connection_options'
|
||||
require 'faraday/options/env'
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# @!parse
|
||||
# # ConnectionOptions contains the configurable properties for a Faraday
|
||||
# # connection object.
|
||||
# class ConnectionOptions < Options; end
|
||||
ConnectionOptions = Options.new(:request, :proxy, :ssl, :builder, :url,
|
||||
:parallel_manager, :params, :headers,
|
||||
:builder_class) do
|
||||
options request: RequestOptions, ssl: SSLOptions
|
||||
|
||||
memoized(:request) { self.class.options_for(:request).new }
|
||||
|
||||
memoized(:ssl) { self.class.options_for(:ssl).new }
|
||||
|
||||
memoized(:builder_class) { RackBuilder }
|
||||
|
||||
def new_builder(block)
|
||||
builder_class.new(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,204 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# @!parse
|
||||
# # @!attribute method
|
||||
# # @return [Symbol] HTTP method (`:get`, `:post`)
|
||||
# #
|
||||
# # @!attribute body
|
||||
# # @return [String] The request body that will eventually be converted to a
|
||||
# # string.
|
||||
# #
|
||||
# # @!attribute url
|
||||
# # @return [URI] URI instance for the current request.
|
||||
# #
|
||||
# # @!attribute request
|
||||
# # @return [Hash] options for configuring the request.
|
||||
# # Options for configuring the request.
|
||||
# #
|
||||
# # - `:timeout` - time limit for the entire request (Integer in
|
||||
# # seconds)
|
||||
# # - `:open_timeout` - time limit for just the connection phase (e.g.
|
||||
# # handshake) (Integer in seconds)
|
||||
# # - `:read_timeout` - time limit for the first response byte received from
|
||||
# # the server (Integer in seconds)
|
||||
# # - `:write_timeout` - time limit for the client to send the request to the
|
||||
# # server (Integer in seconds)
|
||||
# # - `:on_data` - Proc for streaming
|
||||
# # - `:proxy` - Hash of proxy options
|
||||
# # - `:uri` - Proxy server URI
|
||||
# # - `:user` - Proxy server username
|
||||
# # - `:password` - Proxy server password
|
||||
# #
|
||||
# # @!attribute request_headers
|
||||
# # @return [Hash] HTTP Headers to be sent to the server.
|
||||
# #
|
||||
# # @!attribute ssl
|
||||
# # @return [Hash] options for configuring SSL requests
|
||||
# #
|
||||
# # @!attribute parallel_manager
|
||||
# # @return [Object] sent if the connection is in parallel mode
|
||||
# #
|
||||
# # @!attribute params
|
||||
# # @return [Hash]
|
||||
# #
|
||||
# # @!attribute response
|
||||
# # @return [Response]
|
||||
# #
|
||||
# # @!attribute response_headers
|
||||
# # @return [Hash] HTTP headers from the server
|
||||
# #
|
||||
# # @!attribute status
|
||||
# # @return [Integer] HTTP response status code
|
||||
# #
|
||||
# # @!attribute reason_phrase
|
||||
# # @return [String]
|
||||
# class Env < Options; end
|
||||
Env = Options.new(:method, :request_body, :url, :request,
|
||||
:request_headers, :ssl, :parallel_manager, :params,
|
||||
:response, :response_headers, :status,
|
||||
:reason_phrase, :response_body) do
|
||||
const_set(:ContentLength, 'Content-Length')
|
||||
const_set(:StatusesWithoutBody, Set.new([204, 304]))
|
||||
const_set(:SuccessfulStatuses, (200..299))
|
||||
|
||||
# A Set of HTTP verbs that typically send a body. If no body is set for
|
||||
# these requests, the Content-Length header is set to 0.
|
||||
const_set(:MethodsWithBodies, Set.new(Faraday::METHODS_WITH_BODY.map(&:to_sym)))
|
||||
|
||||
options request: RequestOptions,
|
||||
request_headers: Utils::Headers, response_headers: Utils::Headers
|
||||
|
||||
extend Forwardable
|
||||
|
||||
def_delegators :request, :params_encoder
|
||||
|
||||
# Build a new Env from given value. Respects and updates `custom_members`.
|
||||
#
|
||||
# @param value [Object] a value fitting Option.from(v).
|
||||
# @return [Env] from given value
|
||||
def self.from(value)
|
||||
env = super(value)
|
||||
if value.respond_to?(:custom_members)
|
||||
env.custom_members.update(value.custom_members)
|
||||
end
|
||||
env
|
||||
end
|
||||
|
||||
# @param key [Object]
|
||||
def [](key)
|
||||
return self[current_body] if key == :body
|
||||
|
||||
if in_member_set?(key)
|
||||
super(key)
|
||||
else
|
||||
custom_members[key]
|
||||
end
|
||||
end
|
||||
|
||||
# @param key [Object]
|
||||
# @param value [Object]
|
||||
def []=(key, value)
|
||||
if key == :body
|
||||
super(current_body, value)
|
||||
return
|
||||
end
|
||||
|
||||
if in_member_set?(key)
|
||||
super(key, value)
|
||||
else
|
||||
custom_members[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
def current_body
|
||||
!!status ? :response_body : :request_body
|
||||
end
|
||||
|
||||
def body
|
||||
self[:body]
|
||||
end
|
||||
|
||||
def body=(value)
|
||||
self[:body] = value
|
||||
end
|
||||
|
||||
# @return [Boolean] true if status is in the set of {SuccessfulStatuses}.
|
||||
def success?
|
||||
Env::SuccessfulStatuses.include?(status)
|
||||
end
|
||||
|
||||
# @return [Boolean] true if there's no body yet, and the method is in the
|
||||
# set of {Env::MethodsWithBodies}.
|
||||
def needs_body?
|
||||
!body && Env::MethodsWithBodies.include?(method)
|
||||
end
|
||||
|
||||
# Sets content length to zero and the body to the empty string.
|
||||
def clear_body
|
||||
request_headers[Env::ContentLength] = '0'
|
||||
self.body = +''
|
||||
end
|
||||
|
||||
# @return [Boolean] true if the status isn't in the set of
|
||||
# {Env::StatusesWithoutBody}.
|
||||
def parse_body?
|
||||
!Env::StatusesWithoutBody.include?(status)
|
||||
end
|
||||
|
||||
# @return [Boolean] true if there is a parallel_manager
|
||||
def parallel?
|
||||
!!parallel_manager
|
||||
end
|
||||
|
||||
def inspect
|
||||
attrs = [nil]
|
||||
members.each do |mem|
|
||||
if (value = send(mem))
|
||||
attrs << "@#{mem}=#{value.inspect}"
|
||||
end
|
||||
end
|
||||
attrs << "@custom=#{custom_members.inspect}" unless custom_members.empty?
|
||||
%(#<#{self.class}#{attrs.join(' ')}>)
|
||||
end
|
||||
|
||||
def stream_response?
|
||||
request.stream_response?
|
||||
end
|
||||
|
||||
def stream_response(&block)
|
||||
size = 0
|
||||
yielded = false
|
||||
block_result = block.call do |chunk| # rubocop:disable Performance/RedundantBlockCall
|
||||
if chunk.bytesize.positive? || size.positive?
|
||||
yielded = true
|
||||
size += chunk.bytesize
|
||||
request.on_data.call(chunk, size, self)
|
||||
end
|
||||
end
|
||||
request.on_data.call(+'', 0, self) unless yielded
|
||||
block_result
|
||||
end
|
||||
|
||||
# @private
|
||||
def custom_members
|
||||
@custom_members ||= {}
|
||||
end
|
||||
|
||||
# @private
|
||||
if members.first.is_a?(Symbol)
|
||||
def in_member_set?(key)
|
||||
self.class.member_set.include?(key.to_sym)
|
||||
end
|
||||
else
|
||||
def in_member_set?(key)
|
||||
self.class.member_set.include?(key.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
# @private
|
||||
def self.member_set
|
||||
@member_set ||= Set.new(members)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# @!parse
|
||||
# # ProxyOptions contains the configurable properties for the proxy
|
||||
# # configuration used when making an HTTP request.
|
||||
# class ProxyOptions < Options; end
|
||||
ProxyOptions = Options.new(:uri, :user, :password) do
|
||||
extend Forwardable
|
||||
def_delegators :uri, :scheme, :scheme=, :host, :host=, :port, :port=,
|
||||
:path, :path=
|
||||
|
||||
def self.from(value)
|
||||
case value
|
||||
when ''
|
||||
value = nil
|
||||
when String
|
||||
# URIs without a scheme should default to http (like 'example:123').
|
||||
# This fixes #1282 and prevents a silent failure in some adapters.
|
||||
value = "http://#{value}" unless value.include?('://')
|
||||
value = { uri: Utils.URI(value) }
|
||||
when URI
|
||||
value = { uri: value }
|
||||
when Hash, Options
|
||||
if (uri = value.delete(:uri))
|
||||
value[:uri] = Utils.URI(uri)
|
||||
end
|
||||
end
|
||||
|
||||
super(value)
|
||||
end
|
||||
|
||||
memoized(:user) { uri&.user && Utils.unescape(uri.user) }
|
||||
memoized(:password) { uri&.password && Utils.unescape(uri.password) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# @!parse
|
||||
# # RequestOptions contains the configurable properties for a Faraday request.
|
||||
# class RequestOptions < Options; end
|
||||
RequestOptions = Options.new(:params_encoder, :proxy, :bind,
|
||||
:timeout, :open_timeout, :read_timeout,
|
||||
:write_timeout, :boundary, :oauth,
|
||||
:context, :on_data) do
|
||||
def []=(key, value)
|
||||
if key && key.to_sym == :proxy
|
||||
super(key, value ? ProxyOptions.from(value) : nil)
|
||||
else
|
||||
super(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
def stream_response?
|
||||
on_data.is_a?(Proc)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,70 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# @!parse
|
||||
# # SSL-related options.
|
||||
# #
|
||||
# # @!attribute verify
|
||||
# # @return [Boolean] whether to verify SSL certificates or not
|
||||
# #
|
||||
# # @!attribute verify_hostname
|
||||
# # @return [Boolean] whether to enable hostname verification on server certificates
|
||||
# # during the handshake or not (see https://github.com/ruby/openssl/pull/60)
|
||||
# #
|
||||
# # @!attribute ca_file
|
||||
# # @return [String] CA file
|
||||
# #
|
||||
# # @!attribute ca_path
|
||||
# # @return [String] CA path
|
||||
# #
|
||||
# # @!attribute verify_mode
|
||||
# # @return [Integer] Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html)
|
||||
# #
|
||||
# # @!attribute cert_store
|
||||
# # @return [OpenSSL::X509::Store] certificate store
|
||||
# #
|
||||
# # @!attribute client_cert
|
||||
# # @return [String, OpenSSL::X509::Certificate] client certificate
|
||||
# #
|
||||
# # @!attribute client_key
|
||||
# # @return [String, OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] client key
|
||||
# #
|
||||
# # @!attribute certificate
|
||||
# # @return [OpenSSL::X509::Certificate] certificate (Excon only)
|
||||
# #
|
||||
# # @!attribute private_key
|
||||
# # @return [OpenSSL::PKey::RSA, OpenSSL::PKey::DSA] private key (Excon only)
|
||||
# #
|
||||
# # @!attribute verify_depth
|
||||
# # @return [Integer] maximum depth for the certificate chain verification
|
||||
# #
|
||||
# # @!attribute version
|
||||
# # @return [String, Symbol] SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-ssl_version-3D)
|
||||
# #
|
||||
# # @!attribute min_version
|
||||
# # @return [String, Symbol] minimum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-min_version-3D)
|
||||
# #
|
||||
# # @!attribute max_version
|
||||
# # @return [String, Symbol] maximum SSL version (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html#method-i-max_version-3D)
|
||||
# class SSLOptions < Options; end
|
||||
SSLOptions = Options.new(:verify, :verify_hostname,
|
||||
:ca_file, :ca_path, :verify_mode,
|
||||
:cert_store, :client_cert, :client_key,
|
||||
:certificate, :private_key, :verify_depth,
|
||||
:version, :min_version, :max_version) do
|
||||
# @return [Boolean] true if should verify
|
||||
def verify?
|
||||
verify != false
|
||||
end
|
||||
|
||||
# @return [Boolean] true if should not verify
|
||||
def disable?
|
||||
!verify?
|
||||
end
|
||||
|
||||
# @return [Boolean] true if should verify_hostname
|
||||
def verify_hostname?
|
||||
verify_hostname != false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
require 'faraday/encoders/nested_params_encoder'
|
||||
require 'faraday/encoders/flat_params_encoder'
|
||||
@@ -0,0 +1,251 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'faraday/adapter_registry'
|
||||
|
||||
module Faraday
|
||||
# A Builder that processes requests into responses by passing through an inner
|
||||
# middleware stack (heavily inspired by Rack).
|
||||
#
|
||||
# @example
|
||||
# Faraday::Connection.new(url: 'http://httpbingo.org') do |builder|
|
||||
# builder.request :url_encoded # Faraday::Request::UrlEncoded
|
||||
# builder.adapter :net_http # Faraday::Adapter::NetHttp
|
||||
# end
|
||||
class RackBuilder
|
||||
# Used to detect missing arguments
|
||||
NO_ARGUMENT = Object.new
|
||||
|
||||
attr_accessor :handlers
|
||||
|
||||
# Error raised when trying to modify the stack after calling `lock!`
|
||||
class StackLocked < RuntimeError; end
|
||||
|
||||
# borrowed from ActiveSupport::Dependencies::Reference &
|
||||
# ActionDispatch::MiddlewareStack::Middleware
|
||||
class Handler
|
||||
REGISTRY = Faraday::AdapterRegistry.new
|
||||
|
||||
attr_reader :name
|
||||
|
||||
ruby2_keywords def initialize(klass, *args, &block)
|
||||
@name = klass.to_s
|
||||
REGISTRY.set(klass) if klass.respond_to?(:name)
|
||||
@args = args
|
||||
@block = block
|
||||
end
|
||||
|
||||
def klass
|
||||
REGISTRY.get(@name)
|
||||
end
|
||||
|
||||
def inspect
|
||||
@name
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
if other.is_a? Handler
|
||||
name == other.name
|
||||
elsif other.respond_to? :name
|
||||
klass == other
|
||||
else
|
||||
@name == other.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def build(app = nil)
|
||||
klass.new(app, *@args, &@block)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(&block)
|
||||
@adapter = nil
|
||||
@handlers = []
|
||||
build(&block)
|
||||
end
|
||||
|
||||
def initialize_dup(original)
|
||||
super
|
||||
@adapter = original.adapter
|
||||
@handlers = original.handlers.dup
|
||||
end
|
||||
|
||||
def build
|
||||
raise_if_locked
|
||||
block_given? ? yield(self) : request(:url_encoded)
|
||||
adapter(Faraday.default_adapter, **Faraday.default_adapter_options) unless @adapter
|
||||
end
|
||||
|
||||
def [](idx)
|
||||
@handlers[idx]
|
||||
end
|
||||
|
||||
# Locks the middleware stack to ensure no further modifications are made.
|
||||
def lock!
|
||||
@handlers.freeze
|
||||
end
|
||||
|
||||
def locked?
|
||||
@handlers.frozen?
|
||||
end
|
||||
|
||||
ruby2_keywords def use(klass, *args, &block)
|
||||
if klass.is_a? Symbol
|
||||
use_symbol(Faraday::Middleware, klass, *args, &block)
|
||||
else
|
||||
raise_if_locked
|
||||
raise_if_adapter(klass)
|
||||
@handlers << self.class::Handler.new(klass, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
ruby2_keywords def request(key, *args, &block)
|
||||
use_symbol(Faraday::Request, key, *args, &block)
|
||||
end
|
||||
|
||||
ruby2_keywords def response(key, *args, &block)
|
||||
use_symbol(Faraday::Response, key, *args, &block)
|
||||
end
|
||||
|
||||
ruby2_keywords def adapter(klass = NO_ARGUMENT, *args, &block)
|
||||
return @adapter if klass == NO_ARGUMENT || klass.nil?
|
||||
|
||||
klass = Faraday::Adapter.lookup_middleware(klass) if klass.is_a?(Symbol)
|
||||
@adapter = self.class::Handler.new(klass, *args, &block)
|
||||
end
|
||||
|
||||
## methods to push onto the various positions in the stack:
|
||||
|
||||
ruby2_keywords def insert(index, *args, &block)
|
||||
raise_if_locked
|
||||
index = assert_index(index)
|
||||
handler = self.class::Handler.new(*args, &block)
|
||||
@handlers.insert(index, handler)
|
||||
end
|
||||
|
||||
alias insert_before insert
|
||||
|
||||
ruby2_keywords def insert_after(index, *args, &block)
|
||||
index = assert_index(index)
|
||||
insert(index + 1, *args, &block)
|
||||
end
|
||||
|
||||
ruby2_keywords def swap(index, *args, &block)
|
||||
raise_if_locked
|
||||
index = assert_index(index)
|
||||
@handlers.delete_at(index)
|
||||
insert(index, *args, &block)
|
||||
end
|
||||
|
||||
def delete(handler)
|
||||
raise_if_locked
|
||||
@handlers.delete(handler)
|
||||
end
|
||||
|
||||
# Processes a Request into a Response by passing it through this Builder's
|
||||
# middleware stack.
|
||||
#
|
||||
# @param connection [Faraday::Connection]
|
||||
# @param request [Faraday::Request]
|
||||
#
|
||||
# @return [Faraday::Response]
|
||||
def build_response(connection, request)
|
||||
app.call(build_env(connection, request))
|
||||
end
|
||||
|
||||
# The "rack app" wrapped in middleware. All requests are sent here.
|
||||
#
|
||||
# The builder is responsible for creating the app object. After this,
|
||||
# the builder gets locked to ensure no further modifications are made
|
||||
# to the middleware stack.
|
||||
#
|
||||
# Returns an object that responds to `call` and returns a Response.
|
||||
def app
|
||||
@app ||= begin
|
||||
lock!
|
||||
ensure_adapter!
|
||||
to_app
|
||||
end
|
||||
end
|
||||
|
||||
def to_app
|
||||
# last added handler is the deepest and thus closest to the inner app
|
||||
# adapter is always the last one
|
||||
@handlers.reverse.inject(@adapter.build) do |app, handler|
|
||||
handler.build(app)
|
||||
end
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(self.class) &&
|
||||
@handlers == other.handlers &&
|
||||
@adapter == other.adapter
|
||||
end
|
||||
|
||||
# ENV Keys
|
||||
# :http_method - a symbolized request HTTP method (:get, :post)
|
||||
# :body - the request body that will eventually be converted to a string.
|
||||
# :url - URI instance for the current request.
|
||||
# :status - HTTP response status code
|
||||
# :request_headers - hash of HTTP Headers to be sent to the server
|
||||
# :response_headers - Hash of HTTP headers from the server
|
||||
# :parallel_manager - sent if the connection is in parallel mode
|
||||
# :request - Hash of options for configuring the request.
|
||||
# :timeout - open/read timeout Integer in seconds
|
||||
# :open_timeout - read timeout Integer in seconds
|
||||
# :proxy - Hash of proxy options
|
||||
# :uri - Proxy Server URI
|
||||
# :user - Proxy server username
|
||||
# :password - Proxy server password
|
||||
# :ssl - Hash of options for configuring SSL requests.
|
||||
def build_env(connection, request)
|
||||
exclusive_url = connection.build_exclusive_url(
|
||||
request.path, request.params,
|
||||
request.options.params_encoder
|
||||
)
|
||||
|
||||
Env.new(request.http_method, request.body, exclusive_url,
|
||||
request.options, request.headers, connection.ssl,
|
||||
connection.parallel_manager)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
LOCK_ERR = "can't modify middleware stack after making a request"
|
||||
MISSING_ADAPTER_ERROR = "An attempt to run a request with a Faraday::Connection without adapter has been made.\n" \
|
||||
"Please set Faraday.default_adapter or provide one when initializing the connection.\n" \
|
||||
'For more info, check https://lostisland.github.io/faraday/usage/.'
|
||||
|
||||
def raise_if_locked
|
||||
raise StackLocked, LOCK_ERR if locked?
|
||||
end
|
||||
|
||||
def raise_if_adapter(klass)
|
||||
return unless is_adapter?(klass)
|
||||
|
||||
raise 'Adapter should be set using the `adapter` method, not `use`'
|
||||
end
|
||||
|
||||
def ensure_adapter!
|
||||
raise MISSING_ADAPTER_ERROR unless @adapter
|
||||
end
|
||||
|
||||
def adapter_set?
|
||||
!@adapter.nil?
|
||||
end
|
||||
|
||||
def is_adapter?(klass) # rubocop:disable Naming/PredicateName
|
||||
klass <= Faraday::Adapter
|
||||
end
|
||||
|
||||
ruby2_keywords def use_symbol(mod, key, *args, &block)
|
||||
use(mod.lookup_middleware(key), *args, &block)
|
||||
end
|
||||
|
||||
def assert_index(index)
|
||||
idx = index.is_a?(Integer) ? index : @handlers.index(index)
|
||||
raise "No such handler: #{index.inspect}" unless idx
|
||||
|
||||
idx
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,139 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
# Used to setup URLs, params, headers, and the request body in a sane manner.
|
||||
#
|
||||
# @example
|
||||
# @connection.post do |req|
|
||||
# req.url 'http://localhost', 'a' => '1' # 'http://localhost?a=1'
|
||||
# req.headers['b'] = '2' # Header
|
||||
# req.params['c'] = '3' # GET Param
|
||||
# req['b'] = '2' # also Header
|
||||
# req.body = 'abc'
|
||||
# end
|
||||
#
|
||||
# @!attribute http_method
|
||||
# @return [Symbol] the HTTP method of the Request
|
||||
# @!attribute path
|
||||
# @return [URI, String] the path
|
||||
# @!attribute params
|
||||
# @return [Hash] query parameters
|
||||
# @!attribute headers
|
||||
# @return [Faraday::Utils::Headers] headers
|
||||
# @!attribute body
|
||||
# @return [String] body
|
||||
# @!attribute options
|
||||
# @return [RequestOptions] options
|
||||
Request = Struct.new(:http_method, :path, :params, :headers, :body, :options) do
|
||||
extend MiddlewareRegistry
|
||||
|
||||
alias_method :member_get, :[]
|
||||
private :member_get
|
||||
alias_method :member_set, :[]=
|
||||
private :member_set
|
||||
|
||||
# @param request_method [String]
|
||||
# @yield [request] for block customization, if block given
|
||||
# @yieldparam request [Request]
|
||||
# @return [Request]
|
||||
def self.create(request_method)
|
||||
new(request_method).tap do |request|
|
||||
yield(request) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
remove_method :params=
|
||||
# Replace params, preserving the existing hash type.
|
||||
#
|
||||
# @param hash [Hash] new params
|
||||
def params=(hash)
|
||||
if params
|
||||
params.replace hash
|
||||
else
|
||||
member_set(:params, hash)
|
||||
end
|
||||
end
|
||||
|
||||
remove_method :headers=
|
||||
# Replace request headers, preserving the existing hash type.
|
||||
#
|
||||
# @param hash [Hash] new headers
|
||||
def headers=(hash)
|
||||
if headers
|
||||
headers.replace hash
|
||||
else
|
||||
member_set(:headers, hash)
|
||||
end
|
||||
end
|
||||
|
||||
# Update path and params.
|
||||
#
|
||||
# @param path [URI, String]
|
||||
# @param params [Hash, nil]
|
||||
# @return [void]
|
||||
def url(path, params = nil)
|
||||
if path.respond_to? :query
|
||||
if (query = path.query)
|
||||
path = path.dup
|
||||
path.query = nil
|
||||
end
|
||||
else
|
||||
anchor_index = path.index('#')
|
||||
path = path.slice(0, anchor_index) unless anchor_index.nil?
|
||||
path, query = path.split('?', 2)
|
||||
end
|
||||
self.path = path
|
||||
self.params.merge_query query, options.params_encoder
|
||||
self.params.update(params) if params
|
||||
end
|
||||
|
||||
# @param key [Object] key to look up in headers
|
||||
# @return [Object] value of the given header name
|
||||
def [](key)
|
||||
headers[key]
|
||||
end
|
||||
|
||||
# @param key [Object] key of header to write
|
||||
# @param value [Object] value of header
|
||||
def []=(key, value)
|
||||
headers[key] = value
|
||||
end
|
||||
|
||||
# Marshal serialization support.
|
||||
#
|
||||
# @return [Hash] the hash ready to be serialized in Marshal.
|
||||
def marshal_dump
|
||||
{
|
||||
http_method: http_method,
|
||||
body: body,
|
||||
headers: headers,
|
||||
path: path,
|
||||
params: params,
|
||||
options: options
|
||||
}
|
||||
end
|
||||
|
||||
# Marshal serialization support.
|
||||
# Restores the instance variables according to the +serialised+.
|
||||
# @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]
|
||||
end
|
||||
|
||||
# @return [Env] the Env for this Request
|
||||
def to_env(connection)
|
||||
Env.new(http_method, body, connection.build_exclusive_url(path, params),
|
||||
options, headers, connection.ssl, connection.parallel_manager)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'faraday/request/authorization'
|
||||
require 'faraday/request/instrumentation'
|
||||
require 'faraday/request/json'
|
||||
require 'faraday/request/url_encoded'
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
class Request
|
||||
# Request middleware for the Authorization HTTP header
|
||||
class Authorization < Faraday::Middleware
|
||||
KEY = 'Authorization'
|
||||
|
||||
# @param app [#call]
|
||||
# @param type [String, Symbol] Type of Authorization
|
||||
# @param params [Array<String, Proc, #call>] parameters to build the Authorization header.
|
||||
# If the type is `:basic`, then these can be a login and password pair.
|
||||
# Otherwise, a single value is expected that will be appended after the type.
|
||||
# This value can be a proc or an object responding to `.call`, in which case
|
||||
# it will be invoked on each request.
|
||||
def initialize(app, type, *params)
|
||||
@type = type
|
||||
@params = params
|
||||
super(app)
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def on_request(env)
|
||||
return if env.request_headers[KEY]
|
||||
|
||||
env.request_headers[KEY] = header_from(@type, env, *@params)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param type [String, Symbol]
|
||||
# @param env [Faraday::Env]
|
||||
# @param params [Array]
|
||||
# @return [String] a header value
|
||||
def header_from(type, env, *params)
|
||||
if type.to_s.casecmp('basic').zero? && params.size == 2
|
||||
Utils.basic_header_from(*params)
|
||||
elsif params.size != 1
|
||||
raise ArgumentError, "Unexpected params received (got #{params.size} instead of 1)"
|
||||
else
|
||||
value = params.first
|
||||
if (value.is_a?(Proc) && value.arity == 1) || (value.respond_to?(:call) && value.method(:call).arity == 1)
|
||||
value = value.call(env)
|
||||
elsif value.is_a?(Proc) || value.respond_to?(:call)
|
||||
value = value.call
|
||||
end
|
||||
"#{type} #{value}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Request.register_middleware(authorization: Faraday::Request::Authorization)
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
class Request
|
||||
# Middleware for instrumenting Requests.
|
||||
class Instrumentation < Faraday::Middleware
|
||||
# Options class used in Request::Instrumentation class.
|
||||
Options = Faraday::Options.new(:name, :instrumenter) do
|
||||
remove_method :name
|
||||
# @return [String]
|
||||
def name
|
||||
self[:name] ||= 'request.faraday'
|
||||
end
|
||||
|
||||
remove_method :instrumenter
|
||||
# @return [Class]
|
||||
def instrumenter
|
||||
self[:instrumenter] ||= ActiveSupport::Notifications
|
||||
end
|
||||
end
|
||||
|
||||
# Instruments requests using Active Support.
|
||||
#
|
||||
# Measures time spent only for synchronous requests.
|
||||
#
|
||||
# @example Using ActiveSupport::Notifications to measure time spent
|
||||
# for Faraday requests.
|
||||
# ActiveSupport::Notifications
|
||||
# .subscribe('request.faraday') do |name, starts, ends, _, env|
|
||||
# url = env[:url]
|
||||
# http_method = env[:method].to_s.upcase
|
||||
# duration = ends - starts
|
||||
# $stderr.puts '[%s] %s %s (%.3f s)' %
|
||||
# [url.host, http_method, url.request_uri, duration]
|
||||
# end
|
||||
# @param app [#call]
|
||||
# @param options [nil, Hash] Options hash
|
||||
# @option options [String] :name ('request.faraday')
|
||||
# Name of the instrumenter
|
||||
# @option options [Class] :instrumenter (ActiveSupport::Notifications)
|
||||
# Active Support instrumenter class.
|
||||
def initialize(app, options = nil)
|
||||
super(app)
|
||||
@name, @instrumenter = Options.from(options)
|
||||
.values_at(:name, :instrumenter)
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
def call(env)
|
||||
@instrumenter.instrument(@name, env) do
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Request.register_middleware(instrumentation: Faraday::Request::Instrumentation)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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$}
|
||||
|
||||
def on_request(env)
|
||||
match_content_type(env) do |data|
|
||||
env[:body] = encode(data)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def encode(data)
|
||||
if options[:encoder].is_a?(Array) && options[:encoder].size >= 2
|
||||
options[:encoder][0].public_send(options[:encoder][1], data)
|
||||
elsif options[:encoder].respond_to?(:dump)
|
||||
options[:encoder].dump(data)
|
||||
else
|
||||
::JSON.generate(data)
|
||||
end
|
||||
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? || type.match?(MIME_TYPE_REGEX))
|
||||
end
|
||||
|
||||
def body?(env)
|
||||
body = env[:body]
|
||||
case body
|
||||
when true, false
|
||||
true
|
||||
when nil
|
||||
# NOTE: nil can be converted to `"null"`, but this middleware doesn't process `nil` for the compatibility.
|
||||
false
|
||||
else
|
||||
!(body.respond_to?(:to_str) && body.empty?)
|
||||
end
|
||||
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
|
||||
|
||||
Faraday::Request.register_middleware(json: Faraday::Request::Json)
|
||||
@@ -0,0 +1,60 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
class Request
|
||||
# Middleware for supporting urlencoded requests.
|
||||
class UrlEncoded < Faraday::Middleware
|
||||
unless defined?(::Faraday::Request::UrlEncoded::CONTENT_TYPE)
|
||||
CONTENT_TYPE = 'Content-Type'
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_accessor :mime_type
|
||||
end
|
||||
self.mime_type = 'application/x-www-form-urlencoded'
|
||||
|
||||
# Encodes as "application/x-www-form-urlencoded" if not already encoded or
|
||||
# of another type.
|
||||
#
|
||||
# @param env [Faraday::Env]
|
||||
def call(env)
|
||||
match_content_type(env) do |data|
|
||||
params = Faraday::Utils::ParamsHash[data]
|
||||
env.body = params.to_query(env.params_encoder)
|
||||
end
|
||||
@app.call env
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
# @yield [request_body] Body of the request
|
||||
def match_content_type(env)
|
||||
return unless process_request?(env)
|
||||
|
||||
env.request_headers[CONTENT_TYPE] ||= self.class.mime_type
|
||||
return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read)
|
||||
|
||||
yield(env.body)
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
#
|
||||
# @return [Boolean] True if the request has a body and its Content-Type is
|
||||
# urlencoded.
|
||||
def process_request?(env)
|
||||
type = request_type(env)
|
||||
env.body && (type.empty? || (type == self.class.mime_type))
|
||||
end
|
||||
|
||||
# @param env [Faraday::Env]
|
||||
#
|
||||
# @return [String]
|
||||
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
|
||||
|
||||
Faraday::Request.register_middleware(url_encoded: Faraday::Request::UrlEncoded)
|
||||
@@ -0,0 +1,91 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
|
||||
module Faraday
|
||||
# Response represents an HTTP response from making an HTTP request.
|
||||
class Response
|
||||
extend Forwardable
|
||||
extend MiddlewareRegistry
|
||||
|
||||
def initialize(env = nil)
|
||||
@env = Env.from(env) if env
|
||||
@on_complete_callbacks = []
|
||||
end
|
||||
|
||||
attr_reader :env
|
||||
|
||||
def status
|
||||
finished? ? env.status : nil
|
||||
end
|
||||
|
||||
def reason_phrase
|
||||
finished? ? env.reason_phrase : nil
|
||||
end
|
||||
|
||||
def headers
|
||||
finished? ? env.response_headers : {}
|
||||
end
|
||||
|
||||
def_delegator :headers, :[]
|
||||
|
||||
def body
|
||||
finished? ? env.body : nil
|
||||
end
|
||||
|
||||
def finished?
|
||||
!!env
|
||||
end
|
||||
|
||||
def on_complete(&block)
|
||||
if finished?
|
||||
yield(env)
|
||||
else
|
||||
@on_complete_callbacks << block
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def finish(env)
|
||||
raise 'response already finished' if finished?
|
||||
|
||||
@env = env.is_a?(Env) ? env : Env.from(env)
|
||||
@on_complete_callbacks.each { |callback| callback.call(@env) }
|
||||
self
|
||||
end
|
||||
|
||||
def success?
|
||||
finished? && env.success?
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
status: env.status, body: env.body,
|
||||
response_headers: env.response_headers,
|
||||
url: env.url
|
||||
}
|
||||
end
|
||||
|
||||
# because @on_complete_callbacks cannot be marshalled
|
||||
def marshal_dump
|
||||
finished? ? to_hash : nil
|
||||
end
|
||||
|
||||
def marshal_load(env)
|
||||
@env = Env.from(env)
|
||||
end
|
||||
|
||||
# Expand the env with more properties, without overriding existing ones.
|
||||
# Useful for applying request params after restoring a marshalled Response.
|
||||
def apply_request(request_env)
|
||||
raise "response didn't finish yet" unless finished?
|
||||
|
||||
@env = Env.from(request_env).update(@env)
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'faraday/response/json'
|
||||
require 'faraday/response/logger'
|
||||
require 'faraday/response/raise_error'
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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
|
||||
|
||||
process_parser_options
|
||||
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)
|
||||
return if body.strip.empty?
|
||||
|
||||
decoder, method_name = @decoder_options
|
||||
|
||||
decoder.public_send(method_name, body, @parser_options || {})
|
||||
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.match?(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
|
||||
|
||||
def process_parser_options
|
||||
@decoder_options = @parser_options&.delete(:decoder)
|
||||
|
||||
@decoder_options =
|
||||
if @decoder_options.is_a?(Array) && @decoder_options.size >= 2
|
||||
@decoder_options.slice(0, 2)
|
||||
elsif @decoder_options.respond_to?(:load)
|
||||
[@decoder_options, :load]
|
||||
else
|
||||
[::JSON, :parse]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Response.register_middleware(json: Faraday::Response::Json)
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'forwardable'
|
||||
require 'logger'
|
||||
require 'faraday/logging/formatter'
|
||||
|
||||
module Faraday
|
||||
class Response
|
||||
# Logger is a middleware that logs internal events in the HTTP request
|
||||
# lifecycle to a given Logger object. By default, this logs to STDOUT. See
|
||||
# Faraday::Logging::Formatter to see specifically what is logged.
|
||||
class Logger < Middleware
|
||||
def initialize(app, logger = nil, options = {})
|
||||
super(app)
|
||||
logger ||= ::Logger.new($stdout)
|
||||
formatter_class = options.delete(:formatter) || Logging::Formatter
|
||||
@formatter = formatter_class.new(logger: logger, options: options)
|
||||
yield @formatter if block_given?
|
||||
end
|
||||
|
||||
def call(env)
|
||||
@formatter.request(env)
|
||||
super
|
||||
end
|
||||
|
||||
def on_complete(env)
|
||||
@formatter.response(env)
|
||||
end
|
||||
|
||||
def on_error(exc)
|
||||
@formatter.exception(exc) if @formatter.respond_to?(:exception)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Response.register_middleware(logger: Faraday::Response::Logger)
|
||||
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
class Response
|
||||
# RaiseError is a Faraday middleware that raises exceptions on common HTTP
|
||||
# client or server error responses.
|
||||
class RaiseError < Middleware
|
||||
# rubocop:disable Naming/ConstantName
|
||||
ClientErrorStatuses = (400...500)
|
||||
ServerErrorStatuses = (500...600)
|
||||
# rubocop:enable Naming/ConstantName
|
||||
|
||||
def on_complete(env)
|
||||
case env[:status]
|
||||
when 400
|
||||
raise Faraday::BadRequestError, response_values(env)
|
||||
when 401
|
||||
raise Faraday::UnauthorizedError, response_values(env)
|
||||
when 403
|
||||
raise Faraday::ForbiddenError, response_values(env)
|
||||
when 404
|
||||
raise Faraday::ResourceNotFound, response_values(env)
|
||||
when 407
|
||||
# mimic the behavior that we get with proxy requests with HTTPS
|
||||
msg = %(407 "Proxy Authentication Required")
|
||||
raise Faraday::ProxyAuthError.new(msg, response_values(env))
|
||||
when 408
|
||||
raise Faraday::RequestTimeoutError, response_values(env)
|
||||
when 409
|
||||
raise Faraday::ConflictError, response_values(env)
|
||||
when 422
|
||||
raise Faraday::UnprocessableEntityError, response_values(env)
|
||||
when 429
|
||||
raise Faraday::TooManyRequestsError, response_values(env)
|
||||
when ClientErrorStatuses
|
||||
raise Faraday::ClientError, response_values(env)
|
||||
when ServerErrorStatuses
|
||||
raise Faraday::ServerError, response_values(env)
|
||||
when nil
|
||||
raise Faraday::NilStatusError, response_values(env)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a hash of response data with the following keys:
|
||||
# - status
|
||||
# - headers
|
||||
# - body
|
||||
# - request
|
||||
#
|
||||
# The `request` key is omitted when the middleware is explicitly
|
||||
# configured with the option `include_request: false`.
|
||||
def response_values(env)
|
||||
response = {
|
||||
status: env.status,
|
||||
headers: env.response_headers,
|
||||
body: env.body
|
||||
}
|
||||
|
||||
# Include the request data by default. If the middleware was explicitly
|
||||
# configured to _not_ include request data, then omit it.
|
||||
return response unless options.fetch(:include_request, true)
|
||||
|
||||
response.merge(
|
||||
request: {
|
||||
method: env.method,
|
||||
url: env.url,
|
||||
url_path: env.url.path,
|
||||
params: query_params(env),
|
||||
headers: env.request_headers,
|
||||
body: env.request_body
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def query_params(env)
|
||||
env.request.params_encoder ||= Faraday::Utils.default_params_encoder
|
||||
env.params_encoder.decode(env.url.query)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Faraday::Response.register_middleware(raise_error: Faraday::Response::RaiseError)
|
||||
@@ -0,0 +1,121 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'uri'
|
||||
require 'faraday/utils/headers'
|
||||
require 'faraday/utils/params_hash'
|
||||
|
||||
module Faraday
|
||||
# Utils contains various static helper methods.
|
||||
module Utils
|
||||
module_function
|
||||
|
||||
def build_query(params)
|
||||
FlatParamsEncoder.encode(params)
|
||||
end
|
||||
|
||||
def build_nested_query(params)
|
||||
NestedParamsEncoder.encode(params)
|
||||
end
|
||||
|
||||
def default_space_encoding
|
||||
@default_space_encoding ||= '+'
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_writer :default_space_encoding
|
||||
end
|
||||
|
||||
ESCAPE_RE = /[^a-zA-Z0-9 .~_-]/
|
||||
|
||||
def escape(str)
|
||||
str.to_s.gsub(ESCAPE_RE) do |match|
|
||||
"%#{match.unpack('H2' * match.bytesize).join('%').upcase}"
|
||||
end.gsub(' ', default_space_encoding)
|
||||
end
|
||||
|
||||
def unescape(str)
|
||||
CGI.unescape str.to_s
|
||||
end
|
||||
|
||||
DEFAULT_SEP = /[&;] */n
|
||||
|
||||
# Adapted from Rack
|
||||
def parse_query(query)
|
||||
FlatParamsEncoder.decode(query)
|
||||
end
|
||||
|
||||
def parse_nested_query(query)
|
||||
NestedParamsEncoder.decode(query)
|
||||
end
|
||||
|
||||
def default_params_encoder
|
||||
@default_params_encoder ||= NestedParamsEncoder
|
||||
end
|
||||
|
||||
def basic_header_from(login, pass)
|
||||
value = ["#{login}:#{pass}"].pack('m') # Base64 encoding
|
||||
value.delete!("\n")
|
||||
"Basic #{value}"
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_writer :default_params_encoder
|
||||
end
|
||||
|
||||
# Normalize URI() behavior across Ruby versions
|
||||
#
|
||||
# url - A String or URI.
|
||||
#
|
||||
# Returns a parsed URI.
|
||||
def URI(url) # rubocop:disable Naming/MethodName
|
||||
if url.respond_to?(:host)
|
||||
url
|
||||
elsif url.respond_to?(:to_str)
|
||||
default_uri_parser.call(url)
|
||||
else
|
||||
raise ArgumentError, 'bad argument (expected URI object or URI string)'
|
||||
end
|
||||
end
|
||||
|
||||
def default_uri_parser
|
||||
@default_uri_parser ||= Kernel.method(:URI)
|
||||
end
|
||||
|
||||
def default_uri_parser=(parser)
|
||||
@default_uri_parser = if parser.respond_to?(:call) || parser.nil?
|
||||
parser
|
||||
else
|
||||
parser.method(:parse)
|
||||
end
|
||||
end
|
||||
|
||||
# Receives a String or URI and returns just
|
||||
# the path with the query string sorted.
|
||||
def normalize_path(url)
|
||||
url = URI(url)
|
||||
(url.path.start_with?('/') ? url.path : "/#{url.path}") +
|
||||
(url.query ? "?#{sort_query_params(url.query)}" : '')
|
||||
end
|
||||
|
||||
# Recursive hash update
|
||||
def deep_merge!(target, hash)
|
||||
hash.each do |key, value|
|
||||
target[key] = if value.is_a?(Hash) && (target[key].is_a?(Hash) || target[key].is_a?(Options))
|
||||
deep_merge(target[key], value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
target
|
||||
end
|
||||
|
||||
# Recursive hash merge
|
||||
def deep_merge(source, hash)
|
||||
deep_merge!(source.dup, hash)
|
||||
end
|
||||
|
||||
def sort_query_params(query)
|
||||
query.split('&').sort.join('&')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
module Utils
|
||||
# A case-insensitive Hash that preserves the original case of a header
|
||||
# when set.
|
||||
#
|
||||
# Adapted from Rack::Utils::HeaderHash
|
||||
class Headers < ::Hash
|
||||
def self.from(value)
|
||||
new(value)
|
||||
end
|
||||
|
||||
def self.allocate
|
||||
new_self = super
|
||||
new_self.initialize_names
|
||||
new_self
|
||||
end
|
||||
|
||||
def initialize(hash = nil)
|
||||
super()
|
||||
@names = {}
|
||||
update(hash || {})
|
||||
end
|
||||
|
||||
def initialize_names
|
||||
@names = {}
|
||||
end
|
||||
|
||||
# on dup/clone, we need to duplicate @names hash
|
||||
def initialize_copy(other)
|
||||
super
|
||||
@names = other.names.dup
|
||||
end
|
||||
|
||||
# need to synchronize concurrent writes to the shared KeyMap
|
||||
keymap_mutex = Mutex.new
|
||||
|
||||
# symbol -> string mapper + cache
|
||||
KeyMap = Hash.new do |map, key|
|
||||
value = if key.respond_to?(:to_str)
|
||||
key
|
||||
else
|
||||
key.to_s.split('_') # user_agent: %w(user agent)
|
||||
.each(&:capitalize!) # => %w(User Agent)
|
||||
.join('-') # => "User-Agent"
|
||||
end
|
||||
keymap_mutex.synchronize { map[key] = value }
|
||||
end
|
||||
KeyMap[:etag] = 'ETag'
|
||||
|
||||
def [](key)
|
||||
key = KeyMap[key]
|
||||
super(key) || super(@names[key.downcase])
|
||||
end
|
||||
|
||||
def []=(key, val)
|
||||
key = KeyMap[key]
|
||||
key = (@names[key.downcase] ||= key)
|
||||
# join multiple values with a comma
|
||||
val = val.to_ary.join(', ') if val.respond_to?(:to_ary)
|
||||
super(key, val)
|
||||
end
|
||||
|
||||
def fetch(key, *args, &block)
|
||||
key = KeyMap[key]
|
||||
key = @names.fetch(key.downcase, key)
|
||||
super(key, *args, &block)
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
key = KeyMap[key]
|
||||
key = @names[key.downcase]
|
||||
return unless key
|
||||
|
||||
@names.delete key.downcase
|
||||
super(key)
|
||||
end
|
||||
|
||||
def include?(key)
|
||||
@names.include? key.downcase
|
||||
end
|
||||
|
||||
alias has_key? include?
|
||||
alias member? include?
|
||||
alias key? include?
|
||||
|
||||
def merge!(other)
|
||||
other.each { |k, v| self[k] = v }
|
||||
self
|
||||
end
|
||||
|
||||
alias update merge!
|
||||
|
||||
def merge(other)
|
||||
hash = dup
|
||||
hash.merge! other
|
||||
end
|
||||
|
||||
def replace(other)
|
||||
clear
|
||||
@names.clear
|
||||
update other
|
||||
self
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{}.update(self)
|
||||
end
|
||||
|
||||
def parse(header_string)
|
||||
return unless header_string && !header_string.empty?
|
||||
|
||||
headers = header_string.split("\r\n")
|
||||
|
||||
# Find the last set of response headers.
|
||||
start_index = headers.rindex { |x| x.start_with?('HTTP/') } || 0
|
||||
last_response = headers.slice(start_index, headers.size)
|
||||
|
||||
last_response
|
||||
.tap { |a| a.shift if a.first.start_with?('HTTP/') }
|
||||
.map { |h| h.split(/:\s*/, 2) } # split key and value
|
||||
.reject { |p| p[0].nil? } # ignore blank lines
|
||||
.each { |key, value| add_parsed(key, value) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :names
|
||||
|
||||
private
|
||||
|
||||
# Join multiple values with a comma.
|
||||
def add_parsed(key, value)
|
||||
if key?(key)
|
||||
self[key] = self[key].to_s
|
||||
self[key] << ', ' << value
|
||||
else
|
||||
self[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
module Utils
|
||||
# A hash with stringified keys.
|
||||
class ParamsHash < Hash
|
||||
def [](key)
|
||||
super(convert_key(key))
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
super(convert_key(key), value)
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
super(convert_key(key))
|
||||
end
|
||||
|
||||
def include?(key)
|
||||
super(convert_key(key))
|
||||
end
|
||||
|
||||
alias has_key? include?
|
||||
alias member? include?
|
||||
alias key? include?
|
||||
|
||||
def update(params)
|
||||
params.each do |key, value|
|
||||
self[key] = value
|
||||
end
|
||||
self
|
||||
end
|
||||
alias merge! update
|
||||
|
||||
def merge(params)
|
||||
dup.update(params)
|
||||
end
|
||||
|
||||
def replace(other)
|
||||
clear
|
||||
update(other)
|
||||
end
|
||||
|
||||
def merge_query(query, encoder = nil)
|
||||
return self unless query && !query.empty?
|
||||
|
||||
update((encoder || Utils.default_params_encoder).decode(query))
|
||||
end
|
||||
|
||||
def to_query(encoder = nil)
|
||||
(encoder || Utils.default_params_encoder).encode(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_key(key)
|
||||
key.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Faraday
|
||||
VERSION = '2.9.0'
|
||||
end
|
||||
Reference in New Issue
Block a user