http 0.1.0 → 0.2.0
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.
- data/.travis.yml +8 -2
- data/CHANGES.md +6 -0
- data/README.md +5 -10
- data/http.gemspec +5 -3
- data/lib/http.rb +19 -1
- data/lib/http/chainable.rb +36 -12
- data/lib/http/client.rb +37 -74
- data/lib/http/compat/curb.rb +3 -2
- data/lib/http/options.rb +109 -0
- data/lib/http/request.rb +42 -0
- data/lib/http/response.rb +73 -7
- data/lib/http/uri_backport.rb +131 -0
- data/lib/http/version.rb +1 -1
- data/spec/http/compat/curb_spec.rb +1 -1
- data/spec/http/options/callbacks_spec.rb +62 -0
- data/spec/http/options/form_spec.rb +18 -0
- data/spec/http/options/headers_spec.rb +29 -0
- data/spec/http/options/merge_spec.rb +37 -0
- data/spec/http/options/new_spec.rb +39 -0
- data/spec/http/options/response_spec.rb +24 -0
- data/spec/http/options_spec.rb +26 -0
- data/spec/http/response_spec.rb +69 -0
- data/spec/http_spec.rb +24 -2
- data/spec/spec_helper.rb +1 -2
- data/spec/support/{mock_server.rb → example_server.rb} +11 -14
- metadata +80 -85
- data/lib/http/parameters.rb +0 -17
- data/parser/common.rl +0 -40
- data/parser/http.rl +0 -220
- data/parser/multipart.rl +0 -41
data/lib/http/request.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Http
|
2
|
+
class Request
|
3
|
+
# Method is given as a lowercase symbol e.g. :get, :post
|
4
|
+
attr_reader :method
|
5
|
+
|
6
|
+
# "Request URI" as per RFC 2616
|
7
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
|
8
|
+
attr_reader :uri
|
9
|
+
attr_reader :headers, :body, :version
|
10
|
+
|
11
|
+
# :nodoc:
|
12
|
+
def initialize(method, uri, headers = {}, body = nil, version = "1.1")
|
13
|
+
@method = method.to_s.downcase.to_sym
|
14
|
+
raise UnsupportedMethodError, "unknown method: #{method}" unless METHODS.include? @method
|
15
|
+
|
16
|
+
@uri = uri.is_a?(URI) ? uri : URI(uri.to_s)
|
17
|
+
|
18
|
+
@headers = {}
|
19
|
+
headers.each do |name, value|
|
20
|
+
name = name.to_s
|
21
|
+
key = name[CANONICAL_HEADER]
|
22
|
+
key ||= Http.canonicalize_header(name)
|
23
|
+
@headers[key] = value
|
24
|
+
end
|
25
|
+
|
26
|
+
@body, @version = body, version
|
27
|
+
end
|
28
|
+
|
29
|
+
# Obtain the given header
|
30
|
+
def [](header)
|
31
|
+
@headers[Http.canonicalize_header(header)]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Create a Net::HTTP request from this request
|
35
|
+
def to_net_http_request
|
36
|
+
request_class = Net::HTTP.const_get(@method.to_s.capitalize)
|
37
|
+
request = request_class.new(@uri.request_uri, @headers)
|
38
|
+
request.body = @body
|
39
|
+
request
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/http/response.rb
CHANGED
@@ -1,5 +1,62 @@
|
|
1
1
|
module Http
|
2
2
|
class Response
|
3
|
+
STATUS_CODES = {
|
4
|
+
100 => 'Continue',
|
5
|
+
101 => 'Switching Protocols',
|
6
|
+
102 => 'Processing',
|
7
|
+
200 => 'OK',
|
8
|
+
201 => 'Created',
|
9
|
+
202 => 'Accepted',
|
10
|
+
203 => 'Non-Authoritative Information',
|
11
|
+
204 => 'No Content',
|
12
|
+
205 => 'Reset Content',
|
13
|
+
206 => 'Partial Content',
|
14
|
+
207 => 'Multi-Status',
|
15
|
+
226 => 'IM Used',
|
16
|
+
300 => 'Multiple Choices',
|
17
|
+
301 => 'Moved Permanently',
|
18
|
+
302 => 'Found',
|
19
|
+
303 => 'See Other',
|
20
|
+
304 => 'Not Modified',
|
21
|
+
305 => 'Use Proxy',
|
22
|
+
306 => 'Reserved',
|
23
|
+
307 => 'Temporary Redirect',
|
24
|
+
400 => 'Bad Request',
|
25
|
+
401 => 'Unauthorized',
|
26
|
+
402 => 'Payment Required',
|
27
|
+
403 => 'Forbidden',
|
28
|
+
404 => 'Not Found',
|
29
|
+
405 => 'Method Not Allowed',
|
30
|
+
406 => 'Not Acceptable',
|
31
|
+
407 => 'Proxy Authentication Required',
|
32
|
+
408 => 'Request Timeout',
|
33
|
+
409 => 'Conflict',
|
34
|
+
410 => 'Gone',
|
35
|
+
411 => 'Length Required',
|
36
|
+
412 => 'Precondition Failed',
|
37
|
+
413 => 'Request Entity Too Large',
|
38
|
+
414 => 'Request-URI Too Long',
|
39
|
+
415 => 'Unsupported Media Type',
|
40
|
+
416 => 'Requested Range Not Satisfiable',
|
41
|
+
417 => 'Expectation Failed',
|
42
|
+
418 => "I'm a Teapot",
|
43
|
+
422 => 'Unprocessable Entity',
|
44
|
+
423 => 'Locked',
|
45
|
+
424 => 'Failed Dependency',
|
46
|
+
426 => 'Upgrade Required',
|
47
|
+
500 => 'Internal Server Error',
|
48
|
+
501 => 'Not Implemented',
|
49
|
+
502 => 'Bad Gateway',
|
50
|
+
503 => 'Service Unavailable',
|
51
|
+
504 => 'Gateway Timeout',
|
52
|
+
505 => 'HTTP Version Not Supported',
|
53
|
+
506 => 'Variant Also Negotiates',
|
54
|
+
507 => 'Insufficient Storage',
|
55
|
+
510 => 'Not Extended'
|
56
|
+
}
|
57
|
+
|
58
|
+
SYMBOL_TO_STATUS_CODE = Hash[STATUS_CODES.map { |code, msg| [msg.downcase.gsub(/\s|-/, '_').to_sym, code] }]
|
59
|
+
|
3
60
|
attr_accessor :status
|
4
61
|
attr_accessor :headers
|
5
62
|
attr_accessor :body
|
@@ -15,9 +72,13 @@ module Http
|
|
15
72
|
@headers = {}
|
16
73
|
end
|
17
74
|
|
18
|
-
# Set a header
|
19
|
-
def []=(
|
20
|
-
|
75
|
+
# Set a header
|
76
|
+
def []=(name, value)
|
77
|
+
# If we have a canonical header, we're done
|
78
|
+
key = name[CANONICAL_HEADER]
|
79
|
+
|
80
|
+
# Convert to canonical capitalization
|
81
|
+
key ||= Http.canonicalize_header(name)
|
21
82
|
|
22
83
|
# Check if the header has already been set and group
|
23
84
|
old_value = @headers[key]
|
@@ -29,18 +90,23 @@ module Http
|
|
29
90
|
end
|
30
91
|
|
31
92
|
# Get a header value
|
32
|
-
def [](
|
33
|
-
@headers[
|
93
|
+
def [](name)
|
94
|
+
@headers[name] || @headers[Http.canonicalize_header(name)]
|
34
95
|
end
|
35
96
|
|
36
97
|
# Parse the response body according to its content type
|
37
98
|
def parse_body
|
38
|
-
if @headers['
|
39
|
-
mime_type = MimeType[@headers['
|
99
|
+
if @headers['Content-Type']
|
100
|
+
mime_type = MimeType[@headers['Content-Type'].split(/;\s*/).first]
|
40
101
|
return mime_type.parse(@body) if mime_type
|
41
102
|
end
|
42
103
|
|
43
104
|
@body
|
44
105
|
end
|
106
|
+
|
107
|
+
# Returns an Array ala Rack: `[status, headers, body]`
|
108
|
+
def to_a
|
109
|
+
[status, headers, parse_body]
|
110
|
+
end
|
45
111
|
end
|
46
112
|
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# Taken from Ruby 1.9's uri/common.rb
|
2
|
+
# By Akira Yamada <akira@ruby-lang.org>
|
3
|
+
# License:
|
4
|
+
# You can redistribute it and/or modify it under the same term as Ruby.
|
5
|
+
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
# Backport Ruby 1.9's form encoding/decoding functionality
|
9
|
+
module URI
|
10
|
+
TBLENCWWWCOMP_ = {} # :nodoc:
|
11
|
+
256.times do |i|
|
12
|
+
TBLENCWWWCOMP_[i.chr] = '%%%02X' % i
|
13
|
+
end
|
14
|
+
TBLENCWWWCOMP_[' '] = '+'
|
15
|
+
TBLENCWWWCOMP_.freeze
|
16
|
+
TBLDECWWWCOMP_ = {} # :nodoc:
|
17
|
+
256.times do |i|
|
18
|
+
h, l = i>>4, i&15
|
19
|
+
TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr
|
20
|
+
TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr
|
21
|
+
TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr
|
22
|
+
TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr
|
23
|
+
end
|
24
|
+
TBLDECWWWCOMP_['+'] = ' '
|
25
|
+
TBLDECWWWCOMP_.freeze
|
26
|
+
|
27
|
+
# Encode given +str+ to URL-encoded form data.
|
28
|
+
#
|
29
|
+
# This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP
|
30
|
+
# (ASCII space) to + and converts others to %XX.
|
31
|
+
#
|
32
|
+
# This is an implementation of
|
33
|
+
# http://www.w3.org/TR/html5/association-of-controls-and-forms.html#url-encoded-form-data
|
34
|
+
#
|
35
|
+
# See URI.decode_www_form_component, URI.encode_www_form
|
36
|
+
def self.encode_www_form_component(str)
|
37
|
+
str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) { |chr| TBLENCWWWCOMP_[chr] }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Decode given +str+ of URL-encoded form data.
|
41
|
+
#
|
42
|
+
# This decods + to SP.
|
43
|
+
#
|
44
|
+
# See URI.encode_www_form_component, URI.decode_www_form
|
45
|
+
def self.decode_www_form_component(str)
|
46
|
+
raise ArgumentError, "invalid %-encoding (#{str})" unless /\A[^%]*(?:%\h\h[^%]*)*\z/ =~ str
|
47
|
+
str.gsub(/\+|%\h\h/) { |chr| TBLDECWWWCOMP_[chr] }
|
48
|
+
end
|
49
|
+
|
50
|
+
# Generate URL-encoded form data from given +enum+.
|
51
|
+
#
|
52
|
+
# This generates application/x-www-form-urlencoded data defined in HTML5
|
53
|
+
# from given an Enumerable object.
|
54
|
+
#
|
55
|
+
# This internally uses URI.encode_www_form_component(str).
|
56
|
+
#
|
57
|
+
# This method doesn't convert the encoding of given items, so convert them
|
58
|
+
# before call this method if you want to send data as other than original
|
59
|
+
# encoding or mixed encoding data. (Strings which are encoded in an HTML5
|
60
|
+
# ASCII incompatible encoding are converted to UTF-8.)
|
61
|
+
#
|
62
|
+
# This method doesn't handle files. When you send a file, use
|
63
|
+
# multipart/form-data.
|
64
|
+
#
|
65
|
+
# This is an implementation of
|
66
|
+
# http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
67
|
+
#
|
68
|
+
# URI.encode_www_form([["q", "ruby"], ["lang", "en"]])
|
69
|
+
# #=> "q=ruby&lang=en"
|
70
|
+
# URI.encode_www_form("q" => "ruby", "lang" => "en")
|
71
|
+
# #=> "q=ruby&lang=en"
|
72
|
+
# URI.encode_www_form("q" => ["ruby", "perl"], "lang" => "en")
|
73
|
+
# #=> "q=ruby&q=perl&lang=en"
|
74
|
+
# URI.encode_www_form([["q", "ruby"], ["q", "perl"], ["lang", "en"]])
|
75
|
+
# #=> "q=ruby&q=perl&lang=en"
|
76
|
+
#
|
77
|
+
# See URI.encode_www_form_component, URI.decode_www_form
|
78
|
+
def self.encode_www_form(enum)
|
79
|
+
enum.map do |k,v|
|
80
|
+
if v.nil?
|
81
|
+
encode_www_form_component(k)
|
82
|
+
elsif v.respond_to?(:to_ary)
|
83
|
+
v.to_ary.map do |w|
|
84
|
+
str = encode_www_form_component(k)
|
85
|
+
unless w.nil?
|
86
|
+
str << '='
|
87
|
+
str << encode_www_form_component(w)
|
88
|
+
end
|
89
|
+
end.join('&')
|
90
|
+
else
|
91
|
+
str = encode_www_form_component(k)
|
92
|
+
str << '='
|
93
|
+
str << encode_www_form_component(v)
|
94
|
+
end
|
95
|
+
end.join('&')
|
96
|
+
end
|
97
|
+
|
98
|
+
WFKV_ = '(?:[^%#=;&]*(?:%\h\h[^%#=;&]*)*)' # :nodoc:
|
99
|
+
|
100
|
+
# Decode URL-encoded form data from given +str+.
|
101
|
+
#
|
102
|
+
# This decodes application/x-www-form-urlencoded data
|
103
|
+
# and returns array of key-value array.
|
104
|
+
# This internally uses URI.decode_www_form_component.
|
105
|
+
#
|
106
|
+
# _charset_ hack is not supported now because the mapping from given charset
|
107
|
+
# to Ruby's encoding is not clear yet.
|
108
|
+
# see also http://www.w3.org/TR/html5/syntax.html#character-encodings-0
|
109
|
+
#
|
110
|
+
# This refers http://www.w3.org/TR/html5/forms.html#url-encoded-form-data
|
111
|
+
#
|
112
|
+
# ary = URI.decode_www_form("a=1&a=2&b=3")
|
113
|
+
# p ary #=> [['a', '1'], ['a', '2'], ['b', '3']]
|
114
|
+
# p ary.assoc('a').last #=> '1'
|
115
|
+
# p ary.assoc('b').last #=> '3'
|
116
|
+
# p ary.rassoc('a').last #=> '2'
|
117
|
+
# p Hash[ary] # => {"a"=>"2", "b"=>"3"}
|
118
|
+
#
|
119
|
+
# See URI.decode_www_form_component, URI.encode_www_form
|
120
|
+
def self.decode_www_form(str)
|
121
|
+
return [] if str.empty?
|
122
|
+
unless /\A#{WFKV_}=#{WFKV_}(?:[;&]#{WFKV_}=#{WFKV_})*\z/o =~ str
|
123
|
+
raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})"
|
124
|
+
end
|
125
|
+
ary = []
|
126
|
+
$&.scan(/([^=;&]+)=([^;&]*)/) do
|
127
|
+
ary << [decode_www_form_component($1), decode_www_form_component($2)]
|
128
|
+
end
|
129
|
+
ary
|
130
|
+
end
|
131
|
+
end
|
data/lib/http/version.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "callbacks" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
let(:callback){ Proc.new{|r| nil } }
|
7
|
+
|
8
|
+
it 'recognizes invalid events' do
|
9
|
+
lambda{
|
10
|
+
opts.with_callback(:notacallback, callback)
|
11
|
+
}.should raise_error(ArgumentError, /notacallback/)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'recognizes invalid callbacks' do
|
15
|
+
lambda{
|
16
|
+
opts.with_callback(:request, Object.new)
|
17
|
+
}.should raise_error(ArgumentError, /invalid callback/)
|
18
|
+
lambda{
|
19
|
+
opts.with_callback(:request, Proc.new{|a,b| nil})
|
20
|
+
}.should raise_error(ArgumentError, /only one argument/)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "request" do
|
24
|
+
|
25
|
+
it 'defaults to []' do
|
26
|
+
opts.callbacks[:request].should eq([])
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'may be specified with with_callback(:request, ...)' do
|
30
|
+
|
31
|
+
opts2 = opts.with_callback(:request, callback)
|
32
|
+
opts.callbacks[:request].should eq([])
|
33
|
+
opts2.callbacks[:request].should eq([callback])
|
34
|
+
|
35
|
+
opts3 = opts2.with_callback(:request, callback)
|
36
|
+
opts2.callbacks[:request].should eq([callback])
|
37
|
+
opts3.callbacks[:request].should eq([callback, callback])
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "response" do
|
43
|
+
|
44
|
+
it 'defaults to []' do
|
45
|
+
opts.callbacks[:response].should eq([])
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'may be specified with with_callback(:response, ...)' do
|
49
|
+
|
50
|
+
opts2 = opts.with_callback(:response, callback)
|
51
|
+
opts.callbacks[:response].should eq([])
|
52
|
+
opts2.callbacks[:response].should eq([callback])
|
53
|
+
|
54
|
+
opts3 = opts2.with_callback(:response, callback)
|
55
|
+
opts2.callbacks[:response].should eq([callback])
|
56
|
+
opts3.callbacks[:response].should eq([callback, callback])
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "form" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
|
7
|
+
it 'defaults to nil' do
|
8
|
+
opts.form.should be_nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'may be specified with with_form_data' do
|
12
|
+
opts2 = opts.with_form(:foo => 42)
|
13
|
+
opts.form.should be_nil
|
14
|
+
opts2.form.should eq(:foo => 42)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "headers" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
|
7
|
+
it 'defaults to {}' do
|
8
|
+
opts.headers.should eq({})
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'may be specified with with_headers' do
|
12
|
+
opts2 = opts.with_headers("accept" => "json")
|
13
|
+
opts.headers.should eq({})
|
14
|
+
opts2.headers.should eq("accept" => "json")
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'accepts any object that respond to :to_hash' do
|
18
|
+
x = Struct.new(:to_hash).new("accept" => "json")
|
19
|
+
opts.with_headers(x).headers["accept"].should eq("json")
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'recognizes invalid headers' do
|
23
|
+
lambda{
|
24
|
+
opts.with_headers(self)
|
25
|
+
}.should raise_error(ArgumentError)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Http::Options, "merge" do
|
4
|
+
|
5
|
+
let(:opts){ Http::Options.new }
|
6
|
+
|
7
|
+
it 'supports a Hash' do
|
8
|
+
old_response = opts.response
|
9
|
+
opts.merge(:response => :body).response.should eq(:body)
|
10
|
+
opts.response.should eq(old_response)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'supports another Options' do
|
14
|
+
merged = opts.merge(Http::Options.new(:response => :body))
|
15
|
+
merged.response.should eq(:body)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'merges as excepted in complex cases' do
|
19
|
+
foo = Http::Options.new(
|
20
|
+
:response => :body,
|
21
|
+
:form => {:foo => 'foo'},
|
22
|
+
:headers => {:accept => "json", :foo => 'foo'},
|
23
|
+
:callbacks => {:request => ["common"], :response => ["foo"]})
|
24
|
+
bar = Http::Options.new(
|
25
|
+
:response => :parsed_body,
|
26
|
+
:form => {:bar => 'bar'},
|
27
|
+
:headers => {:accept => "xml", :bar => 'bar'},
|
28
|
+
:callbacks => {:request => ["common"], :response => ["bar"]})
|
29
|
+
foo.merge(bar).to_hash.should eq(
|
30
|
+
:response => :parsed_body,
|
31
|
+
:form => {:bar => 'bar'},
|
32
|
+
:headers => {:accept => "xml", :foo => "foo", :bar => 'bar'},
|
33
|
+
:callbacks => {:request => ["common"], :response => ["foo", "bar"]}
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|