mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-04 00:00:37 -04:00
added main building blocks necessary to implement and verify the public API
This commit is contained in:
parent
ef17b68274
commit
b9091db9fa
12
lib/httpx.rb
Normal file
12
lib/httpx.rb
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "httpx/version"
|
||||
|
||||
require "httpx/errors"
|
||||
require "httpx/timeout/null"
|
||||
require "httpx/options"
|
||||
require "httpx/chainable"
|
||||
|
||||
module HTTPX
|
||||
extend Chainable
|
||||
end
|
193
lib/httpx/chainable.rb
Normal file
193
lib/httpx/chainable.rb
Normal file
@ -0,0 +1,193 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "base64"
|
||||
|
||||
module HTTPX
|
||||
module Chainable
|
||||
%i[head get post put delete trace options connect patch].each do |meth|
|
||||
define_method meth do |uri, options = {}|
|
||||
request meth, uri, options
|
||||
end
|
||||
end
|
||||
|
||||
# Make an HTTP request with the given verb
|
||||
# @param uri
|
||||
# @option options [Hash]
|
||||
def request(verb, uri, options = {})
|
||||
branch(options).request verb, uri
|
||||
end
|
||||
|
||||
# @overload timeout(options = {})
|
||||
# Syntax sugar for `timeout(:per_operation, options)`
|
||||
# @overload timeout(klass, options = {})
|
||||
# Adds a timeout to the request.
|
||||
# @param [#to_sym] klass
|
||||
# either :null, :global, or :per_operation
|
||||
# @param [Hash] options
|
||||
# @option options [Float] :read Read timeout
|
||||
# @option options [Float] :write Write timeout
|
||||
# @option options [Float] :connect Connect timeout
|
||||
def timeout(klass, options = {}) # rubocop:disable Style/OptionHash
|
||||
if klass.is_a? Hash
|
||||
options = klass
|
||||
klass = :per_operation
|
||||
end
|
||||
|
||||
klass = case klass.to_sym
|
||||
when :null then HTTPX::Timeout::Null
|
||||
when :global then HTTPX::Timeout::Global
|
||||
when :per_operation then HTTPX::Timeout::PerOperation
|
||||
else raise ArgumentError, "Unsupported Timeout class: #{klass}"
|
||||
end
|
||||
|
||||
%i[read write connect].each do |k|
|
||||
next unless options.key? k
|
||||
options["#{k}_timeout".to_sym] = options.delete k
|
||||
end
|
||||
|
||||
branch default_options.merge(
|
||||
:timeout_class => klass,
|
||||
:timeout_options => options
|
||||
)
|
||||
end
|
||||
|
||||
# @overload persistent(host, timeout: 5)
|
||||
# Flags as persistent
|
||||
# @param [String] host
|
||||
# @option [Integer] timeout Keep alive timeout
|
||||
# @raise [Request::Error] if Host is invalid
|
||||
# @return [HTTPX::Client] Persistent client
|
||||
# @overload persistent(host, timeout: 5, &block)
|
||||
# Executes given block with persistent client and automatically closes
|
||||
# connection at the end of execution.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# def keys(users)
|
||||
# HTTP.persistent("https://github.com") do |http|
|
||||
# users.map { |u| http.get("/#{u}.keys").to_s }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # same as
|
||||
#
|
||||
# def keys(users)
|
||||
# http = HTTP.persistent "https://github.com"
|
||||
# users.map { |u| http.get("/#{u}.keys").to_s }
|
||||
# ensure
|
||||
# http.close if http
|
||||
# end
|
||||
#
|
||||
#
|
||||
# @yieldparam [HTTPX::Client] client Persistent client
|
||||
# @return [Object] result of last expression in the block
|
||||
def persistent(host, timeout: 5)
|
||||
options = {:keep_alive_timeout => timeout}
|
||||
p_client = branch default_options.merge(options).with_persistent host
|
||||
return p_client unless block_given?
|
||||
yield p_client
|
||||
ensure
|
||||
p_client.close if p_client
|
||||
end
|
||||
|
||||
# Make a request through an HTTP proxy
|
||||
# @param [Array] proxy
|
||||
# @raise [Request::Error] if HTTP proxy is invalid
|
||||
def via(*proxy)
|
||||
proxy_hash = {}
|
||||
proxy_hash[:proxy_address] = proxy[0] if proxy[0].is_a?(String)
|
||||
proxy_hash[:proxy_port] = proxy[1] if proxy[1].is_a?(Integer)
|
||||
proxy_hash[:proxy_username] = proxy[2] if proxy[2].is_a?(String)
|
||||
proxy_hash[:proxy_password] = proxy[3] if proxy[3].is_a?(String)
|
||||
proxy_hash[:proxy_headers] = proxy[2] if proxy[2].is_a?(Hash)
|
||||
proxy_hash[:proxy_headers] = proxy[4] if proxy[4].is_a?(Hash)
|
||||
|
||||
raise(RequestError, "invalid HTTP proxy: #{proxy_hash}") unless (2..5).cover?(proxy_hash.keys.size)
|
||||
|
||||
branch default_options.with_proxy(proxy_hash)
|
||||
end
|
||||
alias through via
|
||||
|
||||
# Make client follow redirects.
|
||||
# @param opts
|
||||
# @return [HTTPX::Client]
|
||||
# @see Redirector#initialize
|
||||
def follow(options = {}) # rubocop:disable Style/OptionHash
|
||||
branch default_options.with_follow options
|
||||
end
|
||||
|
||||
# Make a request with the given headers
|
||||
# @param headers
|
||||
def headers(headers)
|
||||
branch default_options.with_headers(headers)
|
||||
end
|
||||
|
||||
# Make a request with the given cookies
|
||||
def cookies(cookies)
|
||||
branch default_options.with_cookies(cookies)
|
||||
end
|
||||
|
||||
# Force a specific encoding for response body
|
||||
def encoding(encoding)
|
||||
branch default_options.with_encoding(encoding)
|
||||
end
|
||||
|
||||
# Accept the given MIME type(s)
|
||||
# @param type
|
||||
def accept(type)
|
||||
headers Headers::ACCEPT => MimeType.normalize(type)
|
||||
end
|
||||
|
||||
# Make a request with the given Authorization header
|
||||
# @param [#to_s] value Authorization header value
|
||||
def auth(value)
|
||||
headers Headers::AUTHORIZATION => value.to_s
|
||||
end
|
||||
|
||||
# Make a request with the given Basic authorization header
|
||||
# @see http://tools.ietf.org/html/rfc2617
|
||||
# @param [#fetch] opts
|
||||
# @option opts [#to_s] :user
|
||||
# @option opts [#to_s] :pass
|
||||
def basic_auth(opts)
|
||||
user = opts.fetch :user
|
||||
pass = opts.fetch :pass
|
||||
|
||||
auth("Basic " + Base64.strict_encode64("#{user}:#{pass}"))
|
||||
end
|
||||
|
||||
# Get options for HTTP
|
||||
# @return [HTTPX::Options]
|
||||
def default_options
|
||||
@default_options ||= HTTPX::Options.new
|
||||
end
|
||||
|
||||
# Set options for HTTP
|
||||
# @param opts
|
||||
# @return [HTTPX::Options]
|
||||
def default_options=(opts)
|
||||
@default_options = HTTPX::Options.new(opts)
|
||||
end
|
||||
|
||||
# Set TCP_NODELAY on the socket
|
||||
def nodelay
|
||||
branch default_options.with_nodelay(true)
|
||||
end
|
||||
|
||||
# Turn on given features. Available features are:
|
||||
# * auto_inflate
|
||||
# * auto_deflate
|
||||
# @param features
|
||||
def use(*features)
|
||||
branch default_options.with_features(features)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# :nodoc:
|
||||
def branch(options)
|
||||
HTTPX::Client.new(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
25
lib/httpx/errors.rb
Normal file
25
lib/httpx/errors.rb
Normal file
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
# Generic error
|
||||
class Error < StandardError; end
|
||||
|
||||
# Generic Connection error
|
||||
class ConnectionError < Error; end
|
||||
|
||||
# Generic Request error
|
||||
class RequestError < Error; end
|
||||
|
||||
# Generic Response error
|
||||
class ResponseError < Error; end
|
||||
|
||||
# Requested to do something when we're in the wrong state
|
||||
class StateError < ResponseError; end
|
||||
|
||||
# Generic Timeout error
|
||||
class TimeoutError < Error; end
|
||||
|
||||
# Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
|
||||
class HeaderError < Error; end
|
||||
end
|
||||
|
166
lib/httpx/headers.rb
Normal file
166
lib/httpx/headers.rb
Normal file
@ -0,0 +1,166 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
class Headers
|
||||
EMPTY = [].freeze # :nodoc:
|
||||
|
||||
# Matches valid header field name according to RFC.
|
||||
# @see http://tools.ietf.org/html/rfc7230#section-3.2
|
||||
COMPLIANT_NAME_RE = /^[A-Za-z0-9!#\$%&'*+\-.^_`|~]+$/
|
||||
|
||||
def initialize(h = nil)
|
||||
@headers = {}
|
||||
return unless h
|
||||
h.each do |field, value|
|
||||
@headers[normalize(field)] = Array(value)
|
||||
end
|
||||
end
|
||||
|
||||
# cloned initialization
|
||||
def initialize_clone(orig)
|
||||
super
|
||||
@headers = orig.instance_variable_get(:@headers).clone
|
||||
end
|
||||
|
||||
# dupped initialization
|
||||
def initialize_dup(orig)
|
||||
super
|
||||
@headers = orig.instance_variable_get(:@headers).dup
|
||||
end
|
||||
|
||||
# freezes the headers hash
|
||||
def freeze
|
||||
@headers.freeze
|
||||
super
|
||||
end
|
||||
|
||||
# merges headers with another header-quack.
|
||||
# the merge rule is, if the header already exists,
|
||||
# ignore what the +other+ headers has. Otherwise, set
|
||||
#
|
||||
def merge(other)
|
||||
# TODO: deep-copy
|
||||
headers = dup
|
||||
other.each do |field, value|
|
||||
headers[field] = value
|
||||
end
|
||||
headers
|
||||
end
|
||||
|
||||
# returns the comma-separated values of the header field
|
||||
# identified by +field+, or nil otherwise.
|
||||
#
|
||||
def [](field)
|
||||
a = @headers[normalize(field)] || return
|
||||
a.join(",")
|
||||
end
|
||||
|
||||
# sets +value+ (if not nil) as single value for the +field+ header.
|
||||
#
|
||||
def []=(field, value)
|
||||
return unless value
|
||||
val = case value
|
||||
when Array
|
||||
value.map { |f| String(f) }
|
||||
else
|
||||
[String(value)]
|
||||
end
|
||||
@headers[normalize(field)] = val
|
||||
end
|
||||
|
||||
# deletes all values associated with +field+ header.
|
||||
#
|
||||
def delete(field)
|
||||
canonical = normalize(field)
|
||||
@headers.delete(canonical) if @headers.key?(canonical)
|
||||
end
|
||||
|
||||
# adds additional +value+ to the existing, for header +field+.
|
||||
#
|
||||
def add(field, value)
|
||||
@headers[normalize(field)] ||= []
|
||||
val = case value
|
||||
when Array
|
||||
value.map { |f| String(f) }
|
||||
else
|
||||
[String(value)]
|
||||
end
|
||||
@headers[normalize(field)] += val
|
||||
end
|
||||
|
||||
# helper to be used when adding an header field as a value to another field
|
||||
#
|
||||
# h2_headers.add_header("vary", "accept-encoding")
|
||||
# h2_headers["vary"] #=> "accept-encoding"
|
||||
# h1_headers.add_header("vary", "accept-encoding")
|
||||
# h1_headers["vary"] #=> "Accept-Encoding"
|
||||
#
|
||||
alias_method :add_header, :add
|
||||
|
||||
# returns the enumerable headers store in pairs of header field + the values in
|
||||
# the comma-separated string format
|
||||
#
|
||||
def each
|
||||
return enum_for(__method__) { @headers.size } unless block_given?
|
||||
@headers.each do |field, value|
|
||||
next if value.empty?
|
||||
value.each do |val|
|
||||
yield(field, val)
|
||||
end
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# the headers store in Hash format
|
||||
def to_hash
|
||||
Hash[to_a]
|
||||
end
|
||||
|
||||
# the headers store in array of pairs format
|
||||
def to_a
|
||||
Array(each)
|
||||
end
|
||||
|
||||
# headers as string
|
||||
def to_s
|
||||
@headers.to_s
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
to_hash == self.class.new(other).to_hash
|
||||
end
|
||||
|
||||
# this is internal API and doesn't abide to other public API
|
||||
# guarantees, like normalizing strings.
|
||||
# Please do not use this outside of core!
|
||||
#
|
||||
def key?(normalized_key)
|
||||
@headers.key?(normalized_key)
|
||||
end
|
||||
|
||||
# returns the values for the +field+ header in array format.
|
||||
# This method is more internal, and for this reason doesn't try
|
||||
# to "correct" the user input, i.e. it doesn't normalize the key.
|
||||
#
|
||||
def get(field)
|
||||
@headers[normalize(field)] || EMPTY
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# this method is only here because there's legacy around using explicit
|
||||
# canonical header fields (ex: Content-Length), although the spec states that the header
|
||||
# fields are case-insensitive.
|
||||
#
|
||||
# This only normalizes a +field+ if passed a canonical HTTP/1 header +field+.
|
||||
#
|
||||
def normalize(field)
|
||||
normalized = String(field).downcase
|
||||
|
||||
return normalized if normalized =~ COMPLIANT_NAME_RE
|
||||
|
||||
raise HeaderError, "Invalid HTTP header field name: #{field.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
122
lib/httpx/options.rb
Normal file
122
lib/httpx/options.rb
Normal file
@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "httpx/headers"
|
||||
|
||||
module HTTPX
|
||||
class Options
|
||||
@default_timeout_class = HTTPX::Timeout::Null
|
||||
|
||||
class << self
|
||||
attr_accessor :default_timeout_class
|
||||
|
||||
def new(options = {})
|
||||
return options if options.is_a?(self)
|
||||
super
|
||||
end
|
||||
|
||||
def defined_options
|
||||
@defined_options ||= []
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def def_option(name, &interpreter)
|
||||
defined_options << name.to_sym
|
||||
interpreter ||= lambda { |v| v }
|
||||
|
||||
attr_accessor name
|
||||
protected :"#{name}="
|
||||
|
||||
define_method(:"with_#{name}") do |value|
|
||||
dup { |opts| opts.send(:"#{name}=", instance_exec(value, &interpreter)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(options = {}) # rubocop:disable Style/OptionHash
|
||||
defaults = {
|
||||
:proxy => {},
|
||||
:timeout_class => self.class.default_timeout_class,
|
||||
:timeout_options => {},
|
||||
:ssl => {},
|
||||
:keep_alive_timeout => 5,
|
||||
:headers => {},
|
||||
:cookies => {},
|
||||
}
|
||||
|
||||
opts_w_defaults = defaults.merge(options)
|
||||
opts_w_defaults[:headers] = HTTPX::Headers.new(opts_w_defaults[:headers])
|
||||
opts_w_defaults.each { |(k, v)| self[k] = v }
|
||||
end
|
||||
|
||||
def_option :headers do |headers|
|
||||
self.headers.merge(headers)
|
||||
end
|
||||
|
||||
def_option :cookies do |cookies|
|
||||
cookies.each_with_object(self.cookies.dup) do |(k, v), jar|
|
||||
cookie = k.is_a?(Cookie) ? k : Cookie.new(k.to_s, v.to_s)
|
||||
jar[cookie.name] = cookie.cookie_value
|
||||
end
|
||||
end
|
||||
|
||||
%w[
|
||||
proxy params form json body follow response
|
||||
ssl_context ssl
|
||||
keep_alive_timeout timeout_class timeout_options
|
||||
].each do |method_name|
|
||||
def_option method_name
|
||||
end
|
||||
|
||||
def follow=(value)
|
||||
@follow =
|
||||
case
|
||||
when !value then nil
|
||||
when true == value then {}
|
||||
when value.respond_to?(:fetch) then value
|
||||
else argument_error! "Unsupported follow options: #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
def merge(other)
|
||||
h1 = to_hash
|
||||
h2 = other.to_hash
|
||||
|
||||
merged = h1.merge(h2) do |k, v1, v2|
|
||||
case k
|
||||
when :headers
|
||||
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 dup
|
||||
dupped = super
|
||||
yield(dupped) if block_given?
|
||||
dupped
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def []=(option, val)
|
||||
send(:"#{option}=", val)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def argument_error!(message)
|
||||
raise(Error, message, caller(1..-1))
|
||||
end
|
||||
end
|
||||
end
|
8
lib/httpx/timeout/null.rb
Normal file
8
lib/httpx/timeout/null.rb
Normal file
@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
module Timeout
|
||||
class Null
|
||||
end
|
||||
end
|
||||
end
|
5
lib/httpx/version.rb
Normal file
5
lib/httpx/version.rb
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTTPX
|
||||
VERSION = "0.0.0"
|
||||
end
|
247
spec/httpx/headers_spec.rb
Normal file
247
spec/httpx/headers_spec.rb
Normal file
@ -0,0 +1,247 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe HTTPX::Headers do
|
||||
subject(:headers) { described_class.new }
|
||||
|
||||
it "can become enumerable" do
|
||||
expect(headers).to respond_to(:each)
|
||||
end
|
||||
|
||||
describe "#[]=" do
|
||||
it "sets header value" do
|
||||
headers["accept"] = "application/json"
|
||||
expect(headers["accept"]).to eq "application/json"
|
||||
end
|
||||
|
||||
it "normalizes header name" do
|
||||
headers["Content-Type"] = "application/json"
|
||||
expect(headers["content-type"]).to eq "application/json"
|
||||
end
|
||||
|
||||
it "overwrites previous value" do
|
||||
headers["set-cookie"] = "hoo=ray"
|
||||
headers["set-cookie"] = "woo=hoo"
|
||||
expect(headers["set-cookie"]).to eq "woo=hoo"
|
||||
end
|
||||
|
||||
it "allows set multiple values" do
|
||||
headers["set-cookie"] = "hoo=ray"
|
||||
headers["set-cookie"] = %w[hoo=ray woo=hoo]
|
||||
expect(headers.get("set-cookie")).to eq %w[hoo=ray woo=hoo]
|
||||
end
|
||||
|
||||
it "fails with empty header name" do
|
||||
expect { headers[""] = "foo bar" }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
|
||||
it "fails with invalid header name" do
|
||||
expect { headers["foo bar"] = "baz" }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
end
|
||||
|
||||
describe "#delete" do
|
||||
before { headers["content-type"] = "application/json" }
|
||||
|
||||
it "removes given header" do
|
||||
headers.delete("content-type")
|
||||
expect(headers["content-type"]).to be_nil
|
||||
end
|
||||
|
||||
it "normalizes header name" do
|
||||
headers.delete("Content-Type")
|
||||
expect(headers["content-type"]).to be_nil
|
||||
end
|
||||
|
||||
it "fails with empty header name" do
|
||||
expect { headers.delete("") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
|
||||
it "fails with invalid header name" do
|
||||
expect { headers.delete("foo bar") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
end
|
||||
|
||||
describe "#add" do
|
||||
it "sets header value" do
|
||||
headers.add "Accept", "application/json"
|
||||
expect(headers["accept"]).to eq "application/json"
|
||||
end
|
||||
|
||||
it "normalizes header name" do
|
||||
headers.add "Content-Type", "application/json"
|
||||
expect(headers["content-type"]).to eq "application/json"
|
||||
end
|
||||
|
||||
it "appends new value if header exists" do
|
||||
headers.add "set-cookie", "hoo=ray"
|
||||
headers.add "set-cookie", "woo=hoo"
|
||||
expect(headers.get("set-cookie")).to eq %w[hoo=ray woo=hoo]
|
||||
end
|
||||
|
||||
it "allows append multiple values" do
|
||||
headers.add "set-cookie", "hoo=ray"
|
||||
headers.add "set-cookie", %w[woo=hoo yup=pie]
|
||||
expect(headers.get("set-cookie")).to eq %w[hoo=ray woo=hoo yup=pie]
|
||||
end
|
||||
|
||||
it "fails with empty header name" do
|
||||
expect { headers.add("", "foobar") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
|
||||
it "fails with invalid header name" do
|
||||
expect { headers.add("foo bar", "baz") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get" do
|
||||
before { headers["Content-Type"] = "application/json" }
|
||||
|
||||
it "returns array of associated values" do
|
||||
expect(headers.get("content-type")).to eq %w[application/json]
|
||||
end
|
||||
|
||||
it "normalizes header name" do
|
||||
expect(headers.get("Content-Type")).to eq %w[application/json]
|
||||
end
|
||||
|
||||
context "when header does not exists" do
|
||||
it "returns empty array" do
|
||||
expect(headers.get("accept")).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
it "fails with empty header name" do
|
||||
expect { headers.get("") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
|
||||
it "fails with invalid header name" do
|
||||
expect { headers.get("foo bar") }.
|
||||
to raise_error HTTPX::HeaderError
|
||||
end
|
||||
end
|
||||
|
||||
describe "#[]" do
|
||||
context "when header does not exists" do
|
||||
it "returns nil" do
|
||||
expect(headers["accept"]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when header has a single value" do
|
||||
before { headers["content-type"] = "application/json" }
|
||||
|
||||
it "normalizes header name" do
|
||||
expect(headers["content-type"]).to_not be_nil
|
||||
end
|
||||
|
||||
it "returns it returns a single value" do
|
||||
expect(headers["content-type"]).to eq "application/json"
|
||||
end
|
||||
end
|
||||
|
||||
context "when header has a multiple values" do
|
||||
before do
|
||||
headers.add "set-cookie", "hoo=ray"
|
||||
headers.add "set-cookie", "woo=hoo"
|
||||
end
|
||||
|
||||
it "normalizes header name" do
|
||||
expect(headers["set-cookie"]).to_not be_nil
|
||||
end
|
||||
|
||||
it "returns array of associated values" do
|
||||
expect(headers.get("set-cookie")).to eq %w[hoo=ray woo=hoo]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#each" do
|
||||
before do
|
||||
headers.add "set-cookie", "hoo=ray"
|
||||
headers.add "content-type", "application/json"
|
||||
headers.add "set-cookie", "woo=hoo"
|
||||
end
|
||||
|
||||
it "yields each key/value pair separatedly" do
|
||||
expect { |b| headers.each(&b) }.to yield_control.exactly(3).times
|
||||
end
|
||||
|
||||
it "yields headers in the same order they were added" do
|
||||
expect { |b| headers.each(&b) }.to yield_successive_args(
|
||||
%w[set-cookie hoo=ray],
|
||||
%w[set-cookie woo=hoo],
|
||||
%w[content-type application/json],
|
||||
)
|
||||
end
|
||||
|
||||
it "returns self instance if block given" do
|
||||
expect(headers.each { |*| }).to be headers
|
||||
end
|
||||
|
||||
it "returns Enumerator if no block given" do
|
||||
expect(headers.each).to be_a Enumerator
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dup" do
|
||||
before { headers["content-type"] = "application/json" }
|
||||
|
||||
subject(:dupped) { headers.dup }
|
||||
|
||||
it { is_expected.to be_a described_class }
|
||||
it { is_expected.not_to be headers }
|
||||
|
||||
it "has headers copied" do
|
||||
expect(dupped["content-type"]).to eq "application/json"
|
||||
end
|
||||
|
||||
context "modifying a copy" do
|
||||
before { dupped["content-type"] = "text/plain" }
|
||||
|
||||
it "modifies dupped copy" do
|
||||
expect(dupped["content-type"]).to eq "text/plain"
|
||||
end
|
||||
|
||||
it "does not affects original headers" do
|
||||
expect(headers["content-type"]).to eq "application/json"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#merge" do
|
||||
before do
|
||||
headers["host"] = "example.com"
|
||||
headers["accept"] = "application/json"
|
||||
end
|
||||
|
||||
subject(:merged) do
|
||||
headers.merge "accept" => "plain/text", "cookie" => %w[hoo=ray woo=hoo]
|
||||
end
|
||||
|
||||
it { is_expected.to be_a described_class }
|
||||
it { is_expected.not_to be headers }
|
||||
|
||||
it "does not affects original headers" do
|
||||
expect(merged).to_not eq headers
|
||||
end
|
||||
|
||||
it "leaves headers not presented in other as is" do
|
||||
expect(merged["host"]).to eq "example.com"
|
||||
end
|
||||
|
||||
it "overwrites existing values" do
|
||||
expect(merged["accept"]).to eq "plain/text"
|
||||
end
|
||||
|
||||
it "appends other headers, not presented in base" do
|
||||
expect(merged.get("cookie")).to eq %w[hoo=ray woo=hoo]
|
||||
end
|
||||
end
|
||||
end
|
176
spec/httpx/options_spec.rb
Normal file
176
spec/httpx/options_spec.rb
Normal file
@ -0,0 +1,176 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe HTTPX::Options do
|
||||
subject { described_class.new(:response => :body) }
|
||||
|
||||
it "has reader methods for attributes" do
|
||||
expect(subject.response).to eq(:body)
|
||||
end
|
||||
|
||||
it "coerces to a Hash" do
|
||||
expect(subject.to_hash).to be_a(Hash)
|
||||
end
|
||||
|
||||
describe "body" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "defaults to nil" do
|
||||
expect(opts.body).to be nil
|
||||
end
|
||||
|
||||
it "may be specified with with_body" do
|
||||
opts2 = opts.with_body("foo")
|
||||
expect(opts.body).to be nil
|
||||
expect(opts2.body).to eq("foo")
|
||||
end
|
||||
end
|
||||
|
||||
describe "form" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "defaults to nil" do
|
||||
expect(opts.form).to be nil
|
||||
end
|
||||
|
||||
it "may be specified with with_form_data" do
|
||||
opts2 = opts.with_form(:foo => 42)
|
||||
expect(opts.form).to be nil
|
||||
expect(opts2.form).to eq(:foo => 42)
|
||||
end
|
||||
end
|
||||
|
||||
describe "headers" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "defaults to be empty" do
|
||||
expect(opts.headers.to_a).to be_empty
|
||||
end
|
||||
|
||||
it "may be specified with with_headers" do
|
||||
opts2 = opts.with_headers("accept" => "json")
|
||||
expect(opts.headers.to_a).to be_empty
|
||||
expect(opts2.headers).to eq([%w[Accept json]])
|
||||
end
|
||||
end
|
||||
|
||||
describe "json" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "defaults to nil" do
|
||||
expect(opts.json).to be nil
|
||||
end
|
||||
|
||||
it "may be specified with with_json data" do
|
||||
opts2 = opts.with_json(:foo => 42)
|
||||
expect(opts.json).to be nil
|
||||
expect(opts2.json).to eq(:foo => 42)
|
||||
end
|
||||
end
|
||||
|
||||
describe "merge" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "supports a Hash" do
|
||||
old_response = opts.response
|
||||
expect(opts.merge(:response => :body).response).to eq(:body)
|
||||
expect(opts.response).to eq(old_response)
|
||||
end
|
||||
|
||||
it "supports another Options" do
|
||||
merged = opts.merge(HTTPX::Options.new(:response => :body))
|
||||
expect(merged.response).to eq(:body)
|
||||
end
|
||||
|
||||
it "merges as excepted in complex cases" do
|
||||
# FIXME: yuck :(
|
||||
|
||||
foo = HTTPX::Options.new(
|
||||
:response => :body,
|
||||
:params => {:baz => "bar"},
|
||||
:form => {:foo => "foo"},
|
||||
:body => "body-foo",
|
||||
:json => {:foo => "foo"},
|
||||
:headers => {"accept" => "json", "foo" => "foo"},
|
||||
:proxy => {},
|
||||
)
|
||||
|
||||
bar = HTTPX::Options.new(
|
||||
:response => :parsed_body,
|
||||
:params => {:plop => "plip"},
|
||||
:form => {:bar => "bar"},
|
||||
:body => "body-bar",
|
||||
:json => {:bar => "bar"},
|
||||
:keep_alive_timeout => 10,
|
||||
:headers => {"accept" => "xml", "bar" => "bar"},
|
||||
:timeout_options => {:foo => :bar},
|
||||
:ssl => {:foo => "bar"},
|
||||
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}
|
||||
)
|
||||
|
||||
expect(foo.merge(bar).to_hash).to eq(
|
||||
:response => :parsed_body,
|
||||
:timeout_class => described_class.default_timeout_class,
|
||||
:timeout_options => {:foo => :bar},
|
||||
:params => {:plop => "plip"},
|
||||
:form => {:bar => "bar"},
|
||||
:body => "body-bar",
|
||||
:json => {:bar => "bar"},
|
||||
:keep_alive_timeout => 10,
|
||||
:ssl => {:foo => "bar"},
|
||||
:headers => HTTPX::Headers.new({"Foo" => "foo", "Accept" => "xml", "Bar" => "bar"}),
|
||||
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080},
|
||||
:follow => nil,
|
||||
:ssl_context => nil,
|
||||
:cookies => {},
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "new" do
|
||||
it "supports a Options instance" do
|
||||
opts = HTTPX::Options.new
|
||||
expect(HTTPX::Options.new(opts)).to eq(opts)
|
||||
end
|
||||
|
||||
context "with a Hash" do
|
||||
it "coerces :response correctly" do
|
||||
opts = HTTPX::Options.new(:response => :object)
|
||||
expect(opts.response).to eq(:object)
|
||||
end
|
||||
|
||||
it "coerces :headers correctly" do
|
||||
opts = HTTPX::Options.new(:headers => {"accept" => "json"})
|
||||
expect(opts.headers).to eq([%w[Accept json]])
|
||||
end
|
||||
|
||||
it "coerces :proxy correctly" do
|
||||
opts = HTTPX::Options.new(:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080})
|
||||
expect(opts.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
||||
end
|
||||
|
||||
it "coerces :form correctly" do
|
||||
opts = HTTPX::Options.new(:form => {:foo => 42})
|
||||
expect(opts.form).to eq(:foo => 42)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "proxy" do
|
||||
let(:opts) { HTTPX::Options.new }
|
||||
|
||||
it "defaults to {}" do
|
||||
expect(opts.proxy).to eq({})
|
||||
end
|
||||
|
||||
it "may be specified with with_proxy" do
|
||||
opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
||||
expect(opts.proxy).to eq({})
|
||||
expect(opts2.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080)
|
||||
end
|
||||
|
||||
it "accepts proxy address, port, username, and password" do
|
||||
opts2 = opts.with_proxy(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password")
|
||||
expect(opts2.proxy).to eq(:proxy_address => "127.0.0.1", :proxy_port => 8080, :proxy_username => "username", :proxy_password => "password")
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user