http 0.5.1 → 0.6.0.pre
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of http might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.gitignore +3 -3
- data/.rspec +3 -2
- data/.rubocop.yml +101 -0
- data/.travis.yml +19 -8
- data/Gemfile +24 -6
- data/LICENSE.txt +1 -1
- data/README.md +144 -29
- data/Rakefile +23 -1
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +14 -14
- data/lib/http.rb +5 -4
- data/lib/http/authorization_header.rb +37 -0
- data/lib/http/authorization_header/basic_auth.rb +24 -0
- data/lib/http/authorization_header/bearer_token.rb +29 -0
- data/lib/http/backports.rb +2 -0
- data/lib/http/backports/base64.rb +6 -0
- data/lib/http/{uri_backport.rb → backports/uri.rb} +10 -10
- data/lib/http/chainable.rb +24 -25
- data/lib/http/client.rb +97 -67
- data/lib/http/content_type.rb +27 -0
- data/lib/http/errors.rb +13 -0
- data/lib/http/headers.rb +154 -0
- data/lib/http/headers/mixin.rb +11 -0
- data/lib/http/mime_type.rb +61 -36
- data/lib/http/mime_type/adapter.rb +24 -0
- data/lib/http/mime_type/json.rb +23 -0
- data/lib/http/options.rb +21 -48
- data/lib/http/redirector.rb +12 -7
- data/lib/http/request.rb +82 -33
- data/lib/http/request/writer.rb +79 -0
- data/lib/http/response.rb +39 -68
- data/lib/http/response/body.rb +62 -0
- data/lib/http/{response_parser.rb → response/parser.rb} +3 -1
- data/lib/http/version.rb +1 -1
- data/logo.png +0 -0
- data/spec/http/authorization_header/basic_auth_spec.rb +29 -0
- data/spec/http/authorization_header/bearer_token_spec.rb +36 -0
- data/spec/http/authorization_header_spec.rb +41 -0
- data/spec/http/backports/base64_spec.rb +13 -0
- data/spec/http/client_spec.rb +181 -0
- data/spec/http/content_type_spec.rb +47 -0
- data/spec/http/headers/mixin_spec.rb +36 -0
- data/spec/http/headers_spec.rb +417 -0
- data/spec/http/options/body_spec.rb +6 -7
- data/spec/http/options/form_spec.rb +4 -5
- data/spec/http/options/headers_spec.rb +9 -17
- data/spec/http/options/json_spec.rb +17 -0
- data/spec/http/options/merge_spec.rb +18 -19
- data/spec/http/options/new_spec.rb +5 -19
- data/spec/http/options/proxy_spec.rb +6 -6
- data/spec/http/options_spec.rb +3 -9
- data/spec/http/redirector_spec.rb +100 -0
- data/spec/http/request/writer_spec.rb +25 -0
- data/spec/http/request_spec.rb +54 -14
- data/spec/http/response/body_spec.rb +24 -0
- data/spec/http/response_spec.rb +61 -32
- data/spec/http_spec.rb +77 -86
- data/spec/spec_helper.rb +25 -2
- data/spec/support/example_server.rb +58 -49
- data/spec/support/proxy_server.rb +27 -11
- metadata +60 -55
- data/lib/http/header.rb +0 -11
- data/lib/http/mime_types/json.rb +0 -19
- data/lib/http/request_stream.rb +0 -77
- data/spec/http/options/callbacks_spec.rb +0 -62
- data/spec/http/options/response_spec.rb +0 -24
- data/spec/http/request_stream_spec.rb +0 -25
@@ -0,0 +1,27 @@
|
|
1
|
+
module HTTP
|
2
|
+
ContentType = Struct.new(:mime_type, :charset) do
|
3
|
+
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
|
4
|
+
CHARSET_RE = /;\s*charset=([^;]+)/i
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Parse string and return ContentType struct
|
8
|
+
def parse(str)
|
9
|
+
new mime_type(str), charset(str)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
# :nodoc:
|
15
|
+
def mime_type(str)
|
16
|
+
md = str.to_s.match MIME_TYPE_RE
|
17
|
+
md && md[1].to_s.strip.downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
# :nodoc:
|
21
|
+
def charset(str)
|
22
|
+
md = str.to_s.match CHARSET_RE
|
23
|
+
md && md[1].to_s.strip.gsub(/^"|"$/, '')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/http/errors.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module HTTP
|
2
|
+
# Generic error
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Generic Request error
|
6
|
+
class RequestError < Error; end
|
7
|
+
|
8
|
+
# Generic Response error
|
9
|
+
class ResponseError < Error; end
|
10
|
+
|
11
|
+
# Request to do something when we're in the wrong state
|
12
|
+
class StateError < ResponseError; end
|
13
|
+
end
|
data/lib/http/headers.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require 'http/headers/mixin'
|
4
|
+
|
5
|
+
module HTTP
|
6
|
+
class Headers
|
7
|
+
extend Forwardable
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# Matches HTTP header names when in "Canonical-Http-Format"
|
11
|
+
CANONICAL_HEADER = /^[A-Z][a-z]*(-[A-Z][a-z]*)*$/
|
12
|
+
|
13
|
+
# :nodoc:
|
14
|
+
def initialize
|
15
|
+
@pile = []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets header
|
19
|
+
#
|
20
|
+
# @return [void]
|
21
|
+
def set(name, value)
|
22
|
+
delete(name)
|
23
|
+
add(name, value)
|
24
|
+
end
|
25
|
+
alias_method :[]=, :set
|
26
|
+
|
27
|
+
# Removes header
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def delete(name)
|
31
|
+
name = canonicalize_header name.to_s
|
32
|
+
@pile.delete_if { |k, _| k == name }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Append header
|
36
|
+
#
|
37
|
+
# @return [void]
|
38
|
+
def add(name, value)
|
39
|
+
name = canonicalize_header name.to_s
|
40
|
+
Array(value).each { |v| @pile << [name, v] }
|
41
|
+
end
|
42
|
+
alias_method :append, :add
|
43
|
+
|
44
|
+
# Return array of header values if any.
|
45
|
+
#
|
46
|
+
# @return [Array]
|
47
|
+
def get(name)
|
48
|
+
name = canonicalize_header name.to_s
|
49
|
+
@pile.select { |k, _| k == name }.map { |_, v| v }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Smart version of {#get}
|
53
|
+
#
|
54
|
+
# @return [NilClass] if header was not set
|
55
|
+
# @return [Object] if header has exactly one value
|
56
|
+
# @return [Array<Object>] if header has more than one value
|
57
|
+
def [](name)
|
58
|
+
values = get(name)
|
59
|
+
|
60
|
+
case values.count
|
61
|
+
when 0 then nil
|
62
|
+
when 1 then values.first
|
63
|
+
else values
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Converts headers into a Rack-compatible Hash
|
68
|
+
#
|
69
|
+
# @return [Hash]
|
70
|
+
def to_h
|
71
|
+
Hash[keys.map { |k| [k, self[k]] }]
|
72
|
+
end
|
73
|
+
|
74
|
+
# Array of key/value pairs
|
75
|
+
#
|
76
|
+
# @return [Array<[String, String]>]
|
77
|
+
def to_a
|
78
|
+
@pile.map { |pair| pair.map(&:dup) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# :nodoc:
|
82
|
+
def inspect
|
83
|
+
"#<#{self.class} #{to_h.inspect}>"
|
84
|
+
end
|
85
|
+
|
86
|
+
# List of header names
|
87
|
+
#
|
88
|
+
# @return [Array<String>]
|
89
|
+
def keys
|
90
|
+
@pile.map { |k, _| k }.uniq
|
91
|
+
end
|
92
|
+
|
93
|
+
# Compares headers to another Headers or Array of key/value pairs
|
94
|
+
#
|
95
|
+
# @return [Boolean]
|
96
|
+
def ==(other)
|
97
|
+
return false unless other.respond_to? :to_a
|
98
|
+
@pile == other.to_a
|
99
|
+
end
|
100
|
+
|
101
|
+
def_delegators :@pile, :each, :empty?, :hash
|
102
|
+
|
103
|
+
# :nodoc:
|
104
|
+
def initialize_copy(orig)
|
105
|
+
super
|
106
|
+
@pile = to_a
|
107
|
+
end
|
108
|
+
|
109
|
+
# Merge in `other` headers
|
110
|
+
#
|
111
|
+
# @see #merge
|
112
|
+
# @return [void]
|
113
|
+
def merge!(other)
|
114
|
+
self.class.coerce(other).to_h.each { |name, values| set name, values }
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns new Headers instance with `other` headers merged in.
|
118
|
+
#
|
119
|
+
# @see #merge!
|
120
|
+
# @return [Headers]
|
121
|
+
def merge(other)
|
122
|
+
dup.tap { |dupped| dupped.merge! other }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Initiates new Headers object from given object.
|
126
|
+
#
|
127
|
+
# @raise [Error] if given object can't be coerced
|
128
|
+
# @param [#to_hash, #to_h, #to_a] object
|
129
|
+
# @return [Headers]
|
130
|
+
def self.coerce(object)
|
131
|
+
unless object.is_a? self
|
132
|
+
object = case
|
133
|
+
when object.respond_to?(:to_hash) then object.to_hash
|
134
|
+
when object.respond_to?(:to_h) then object.to_h
|
135
|
+
when object.respond_to?(:to_a) then object.to_a
|
136
|
+
else fail Error, "Can't coerce #{object.inspect} to Headers"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
headers = new
|
141
|
+
object.each { |k, v| headers.add k, v }
|
142
|
+
headers
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
# Transform to canonical HTTP header capitalization
|
148
|
+
# @param [String] name
|
149
|
+
# @return [String]
|
150
|
+
def canonicalize_header(name)
|
151
|
+
name[CANONICAL_HEADER] || name.split(/[\-_]/).map(&:capitalize).join('-')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/http/mime_type.rb
CHANGED
@@ -1,51 +1,76 @@
|
|
1
1
|
module HTTP
|
2
|
-
#
|
3
|
-
|
4
|
-
class MimeType
|
5
|
-
@mime_types, @shortcuts = {}, {}
|
6
|
-
|
2
|
+
# MIME type encode/decode adapters
|
3
|
+
module MimeType
|
7
4
|
class << self
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
# Associate MIME type with adapter
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
#
|
9
|
+
# module JsonAdapter
|
10
|
+
# class << self
|
11
|
+
# def encode(obj)
|
12
|
+
# # encode logic here
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# def decode(str)
|
16
|
+
# # decode logic here
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# HTTP::MimeType.register_adapter 'application/json', MyJsonAdapter
|
22
|
+
#
|
23
|
+
# @param [#to_s] type
|
24
|
+
# @param [#encode, #decode] adapter
|
25
|
+
# @return [void]
|
26
|
+
def register_adapter(type, adapter)
|
27
|
+
adapters[type.to_s] = adapter
|
11
28
|
end
|
12
29
|
|
30
|
+
# Returns adapter associated with MIME type
|
31
|
+
#
|
32
|
+
# @param [#to_s] type
|
33
|
+
# @raise [Error] if no adapter found
|
34
|
+
# @return [void]
|
13
35
|
def [](type)
|
14
|
-
|
15
|
-
@shortcuts[type]
|
16
|
-
else
|
17
|
-
@mime_types[type]
|
18
|
-
end
|
36
|
+
adapters[normalize type] || fail(Error, "Unknown MIME type: #{type}")
|
19
37
|
end
|
20
|
-
end
|
21
|
-
|
22
|
-
attr_reader :type, :shortcut
|
23
38
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
# Register a shortcut for MIME type
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
#
|
43
|
+
# HTTP::MimeType.register_alias 'application/json', :json
|
44
|
+
#
|
45
|
+
# @param [#to_s] type
|
46
|
+
# @param [#to_sym] shortcut
|
47
|
+
# @return [void]
|
48
|
+
def register_alias(type, shortcut)
|
49
|
+
aliases[shortcut.to_sym] = type.to_s
|
50
|
+
end
|
30
51
|
|
31
|
-
|
32
|
-
|
33
|
-
@
|
34
|
-
|
52
|
+
# Resolves type by shortcut if possible
|
53
|
+
#
|
54
|
+
# @param [#to_s] type
|
55
|
+
# @return [String]
|
56
|
+
def normalize(type)
|
57
|
+
aliases.fetch type, type.to_s
|
58
|
+
end
|
35
59
|
|
36
|
-
|
37
|
-
@emit_with = block
|
38
|
-
end
|
60
|
+
private
|
39
61
|
|
40
|
-
|
41
|
-
|
42
|
-
|
62
|
+
# :nodoc:
|
63
|
+
def adapters
|
64
|
+
@adapters ||= {}
|
65
|
+
end
|
43
66
|
|
44
|
-
|
45
|
-
|
67
|
+
# :nodoc:
|
68
|
+
def aliases
|
69
|
+
@aliases ||= {}
|
70
|
+
end
|
46
71
|
end
|
47
72
|
end
|
48
73
|
end
|
49
74
|
|
50
|
-
#
|
51
|
-
require 'http/
|
75
|
+
# built-in mime types
|
76
|
+
require 'http/mime_type/json'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module HTTP
|
5
|
+
module MimeType
|
6
|
+
# Base encode/decode MIME type adapter
|
7
|
+
class Adapter
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
class << self
|
11
|
+
extend Forwardable
|
12
|
+
def_delegators :instance, :encode, :decode
|
13
|
+
end
|
14
|
+
|
15
|
+
%w[encode decode].each do |operation|
|
16
|
+
class_eval <<-RUBY, __FILE__, __LINE__
|
17
|
+
def #{operation}(*)
|
18
|
+
fail Error, "\#{self.class} does not supports ##{operation}"
|
19
|
+
end
|
20
|
+
RUBY
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'http/mime_type/adapter'
|
3
|
+
|
4
|
+
module HTTP
|
5
|
+
module MimeType
|
6
|
+
# JSON encode/decode MIME type adapter
|
7
|
+
class JSON < Adapter
|
8
|
+
# Encodes object to JSON
|
9
|
+
def encode(obj)
|
10
|
+
return obj.to_json if obj.respond_to?(:to_json)
|
11
|
+
::JSON.dump obj
|
12
|
+
end
|
13
|
+
|
14
|
+
# Decodes JSON
|
15
|
+
def decode(str)
|
16
|
+
::JSON.load str
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
register_adapter 'application/json', JSON
|
21
|
+
register_alias 'application/json', :json
|
22
|
+
end
|
23
|
+
end
|
data/lib/http/options.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
require 'http/
|
1
|
+
require 'http/headers'
|
2
2
|
require 'openssl'
|
3
3
|
require 'socket'
|
4
4
|
|
5
5
|
module HTTP
|
6
6
|
class Options
|
7
|
-
|
8
7
|
# How to format the response [:object, :body, :parse_body]
|
9
8
|
attr_accessor :response
|
10
9
|
|
@@ -17,15 +16,15 @@ module HTTP
|
|
17
16
|
# Form data to embed in the request
|
18
17
|
attr_accessor :form
|
19
18
|
|
19
|
+
# JSON data to embed in the request
|
20
|
+
attr_accessor :json
|
21
|
+
|
20
22
|
# Explicit request body of the request
|
21
23
|
attr_accessor :body
|
22
24
|
|
23
25
|
# HTTP proxy to route request
|
24
26
|
attr_accessor :proxy
|
25
27
|
|
26
|
-
# Before callbacks
|
27
|
-
attr_accessor :callbacks
|
28
|
-
|
29
28
|
# Socket classes
|
30
29
|
attr_accessor :socket_class, :ssl_socket_class
|
31
30
|
|
@@ -35,7 +34,7 @@ module HTTP
|
|
35
34
|
# Follow redirects
|
36
35
|
attr_accessor :follow
|
37
36
|
|
38
|
-
protected :response=, :headers=, :proxy=, :params=, :form=,
|
37
|
+
protected :response=, :headers=, :proxy=, :params=, :form=, :json=, :follow=
|
39
38
|
|
40
39
|
@default_socket_class = TCPSocket
|
41
40
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
@@ -51,36 +50,23 @@ module HTTP
|
|
51
50
|
|
52
51
|
def initialize(options = {})
|
53
52
|
@response = options[:response] || :auto
|
54
|
-
@headers = options[:headers] || {}
|
55
53
|
@proxy = options[:proxy] || {}
|
56
|
-
@callbacks = options[:callbacks] || {:request => [], :response => []}
|
57
54
|
@body = options[:body]
|
58
|
-
@params
|
55
|
+
@params = options[:params]
|
59
56
|
@form = options[:form]
|
57
|
+
@json = options[:json]
|
60
58
|
@follow = options[:follow]
|
61
59
|
|
60
|
+
@headers = HTTP::Headers.coerce(options[:headers] || {})
|
61
|
+
|
62
62
|
@socket_class = options[:socket_class] || self.class.default_socket_class
|
63
63
|
@ssl_socket_class = options[:ssl_socket_class] || self.class.default_ssl_socket_class
|
64
64
|
@ssl_context = options[:ssl_context]
|
65
|
-
|
66
|
-
@headers["User-Agent"] ||= "RubyHTTPGem/#{HTTP::VERSION}"
|
67
|
-
end
|
68
|
-
|
69
|
-
def with_response(response)
|
70
|
-
unless [:auto, :object, :body, :parsed_body].include?(response)
|
71
|
-
argument_error! "invalid response type: #{response}"
|
72
|
-
end
|
73
|
-
dup do |opts|
|
74
|
-
opts.response = response
|
75
|
-
end
|
76
65
|
end
|
77
66
|
|
78
67
|
def with_headers(headers)
|
79
|
-
unless headers.respond_to?(:to_hash)
|
80
|
-
argument_error! "invalid headers: #{headers}"
|
81
|
-
end
|
82
68
|
dup do |opts|
|
83
|
-
opts.headers = self.headers.merge(headers
|
69
|
+
opts.headers = self.headers.merge(headers)
|
84
70
|
end
|
85
71
|
end
|
86
72
|
|
@@ -102,31 +88,21 @@ module HTTP
|
|
102
88
|
end
|
103
89
|
end
|
104
90
|
|
105
|
-
def
|
91
|
+
def with_json(data)
|
106
92
|
dup do |opts|
|
107
|
-
opts.
|
93
|
+
opts.json = data
|
108
94
|
end
|
109
95
|
end
|
110
96
|
|
111
|
-
def
|
97
|
+
def with_body(body)
|
112
98
|
dup do |opts|
|
113
|
-
opts.
|
99
|
+
opts.body = body
|
114
100
|
end
|
115
101
|
end
|
116
102
|
|
117
|
-
def
|
118
|
-
unless callback.respond_to?(:call)
|
119
|
-
argument_error! "invalid callback: #{callback}"
|
120
|
-
end
|
121
|
-
unless callback.respond_to?(:arity) and callback.arity == 1
|
122
|
-
argument_error! "callback must accept only one argument"
|
123
|
-
end
|
124
|
-
unless [:request, :response].include?(event)
|
125
|
-
argument_error! "invalid callback event: #{event}"
|
126
|
-
end
|
103
|
+
def with_follow(follow)
|
127
104
|
dup do |opts|
|
128
|
-
opts.
|
129
|
-
opts.callbacks[event] = (callbacks[event].dup << callback)
|
105
|
+
opts.follow = follow
|
130
106
|
end
|
131
107
|
end
|
132
108
|
|
@@ -136,12 +112,10 @@ module HTTP
|
|
136
112
|
|
137
113
|
def merge(other)
|
138
114
|
h1, h2 = to_hash, other.to_hash
|
139
|
-
merged = h1.merge(h2) do |k,v1,v2|
|
115
|
+
merged = h1.merge(h2) do |k, v1, v2|
|
140
116
|
case k
|
141
117
|
when :headers
|
142
118
|
v1.merge(v2)
|
143
|
-
when :callbacks
|
144
|
-
v1.merge(v2){|event,l,r| (l+r).uniq}
|
145
119
|
else
|
146
120
|
v2
|
147
121
|
end
|
@@ -156,12 +130,12 @@ module HTTP
|
|
156
130
|
# get serialized here, rather than manually having to add them each time
|
157
131
|
{
|
158
132
|
:response => response,
|
159
|
-
:headers => headers,
|
133
|
+
:headers => headers.to_h,
|
160
134
|
:proxy => proxy,
|
161
135
|
:params => params,
|
162
136
|
:form => form,
|
137
|
+
:json => json,
|
163
138
|
:body => body,
|
164
|
-
:callbacks => callbacks,
|
165
139
|
:follow => follow,
|
166
140
|
:socket_class => socket_class,
|
167
141
|
:ssl_socket_class => ssl_socket_class,
|
@@ -175,11 +149,10 @@ module HTTP
|
|
175
149
|
dupped
|
176
150
|
end
|
177
151
|
|
178
|
-
|
152
|
+
private
|
179
153
|
|
180
154
|
def argument_error!(message)
|
181
|
-
|
155
|
+
fail(Error, message, caller[1..-1])
|
182
156
|
end
|
183
|
-
|
184
157
|
end
|
185
158
|
end
|