lib/httpx/options.rb



# frozen_string_literal: true

module HTTPX
  class Options
    MAX_CONCURRENT_REQUESTS = 100
    WINDOW_SIZE = 1 << 14 # 16K
    MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K

    class << self
      def inherited(klass)
        super
        klass.instance_variable_set(:@defined_options, @defined_options.dup)
      end

      def new(options = {})
        # let enhanced options go through
        return options if self == Options && options.class > self
        return options if options.is_a?(self)

        super
      end

      def defined_options
        @defined_options ||= []
      end

      def def_option(name, &interpreter)
        defined_options << name.to_sym
        interpreter ||= ->(v) { v }

        attr_accessor name
        protected :"#{name}="

        define_method(:"with_#{name}") do |value|
          other = dup
          other.send(:"#{name}=", other.instance_exec(value, &interpreter))
          other
        end
      end
    end

    def initialize(options = {})
      defaults = {
        :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
        :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
        :ssl => {},
        :http2_settings => { settings_enable_push: 0 },
        :fallback_protocol => "http/1.1",
        :timeout => Timeout.new,
        :headers => {},
        :max_concurrent_requests => MAX_CONCURRENT_REQUESTS,
        :window_size => WINDOW_SIZE,
        :body_threshold_size => MAX_BODY_THRESHOLD_SIZE,
        :request_class => Class.new(Request),
        :response_class => Class.new(Response),
        :headers_class => Class.new(Headers),
        :request_body_class => Class.new(Request::Body),
        :response_body_class => Class.new(Response::Body),
        :connection_class => Class.new(Connection),
        :transport => nil,
        :transport_options => nil,
        :persistent => false,
        :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
        :resolver_options => { cache: true },
      }

      defaults.merge!(options)
      defaults[:headers] = Headers.new(defaults[:headers])
      defaults.each { |(k, v)| self[k] = v }
    end

    def_option(:headers) do |headers|
      self.headers.merge(headers)
    end

    def_option(:timeout) do |opts|
      self.timeout = Timeout.new(opts)
    end

    def_option(:max_concurrent_requests) do |num|
      max = Integer(num)
      raise Error, ":max_concurrent_requests must be positive" unless max.positive?

      self.max_concurrent_requests = max
    end

    def_option(:window_size) do |num|
      self.window_size = Integer(num)
    end

    def_option(:body_threshold_size) do |num|
      self.body_threshold_size = Integer(num)
    end

    def_option(:transport) do |tr|
      transport = tr.to_s
      raise Error, "#{transport} is an unsupported transport type" unless IO.registry.key?(transport)

      self.transport = transport
    end

    %w[
      params form json body
      follow ssl http2_settings
      request_class response_class headers_class request_body_class response_body_class connection_class
      io fallback_protocol debug debug_level transport_options resolver_class resolver_options
      persistent
    ].each do |method_name|
      def_option(method_name)
    end

    REQUEST_IVARS = %i[@params @form @json @body].freeze

    def ==(other)
      ivars = instance_variables | other.instance_variables
      ivars.all? do |ivar|
        case ivar
        when :@headers
          headers = instance_variable_get(ivar)
          headers.same_headers?(other.instance_variable_get(ivar))
        when *REQUEST_IVARS
          true
        else
          instance_variable_get(ivar) == other.instance_variable_get(ivar)
        end
      end
    end

    def merge(other)
      h1 = to_hash
      h2 = other.to_hash

      merged = h1.merge(h2) do |k, v1, v2|
        case k
        when :headers, :ssl, :http2_settings, :timeout
          v1.merge(v2)
        else
          v2
        end
      end

      self.class.new(merged)
    end

    def to_hash
      hash_pairs = self.class
                       .defined_options
                       .flat_map { |opt_name| [opt_name, send(opt_name)] }
      Hash[*hash_pairs]
    end

    def initialize_dup(other)
      self.headers             = other.headers.dup
      self.ssl                 = other.ssl.dup
      self.request_class       = other.request_class.dup
      self.response_class      = other.response_class.dup
      self.headers_class       = other.headers_class.dup
      self.request_body_class  = other.request_body_class.dup
      self.response_body_class = other.response_body_class.dup
      self.connection_class    = other.connection_class.dup
    end

    def freeze
      super

      headers.freeze
      ssl.freeze
      request_class.freeze
      response_class.freeze
      headers_class.freeze
      request_body_class.freeze
      response_body_class.freeze
      connection_class.freeze
    end

    protected

    def []=(option, val)
      send(:"#{option}=", val)
    end
  end
end