mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-10-05 00:02:38 -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