# frozen_string_literal: true require 'ruby2_keywords' 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