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.

Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -3
  3. data/.rspec +3 -2
  4. data/.rubocop.yml +101 -0
  5. data/.travis.yml +19 -8
  6. data/Gemfile +24 -6
  7. data/LICENSE.txt +1 -1
  8. data/README.md +144 -29
  9. data/Rakefile +23 -1
  10. data/examples/parallel_requests_with_celluloid.rb +2 -2
  11. data/http.gemspec +14 -14
  12. data/lib/http.rb +5 -4
  13. data/lib/http/authorization_header.rb +37 -0
  14. data/lib/http/authorization_header/basic_auth.rb +24 -0
  15. data/lib/http/authorization_header/bearer_token.rb +29 -0
  16. data/lib/http/backports.rb +2 -0
  17. data/lib/http/backports/base64.rb +6 -0
  18. data/lib/http/{uri_backport.rb → backports/uri.rb} +10 -10
  19. data/lib/http/chainable.rb +24 -25
  20. data/lib/http/client.rb +97 -67
  21. data/lib/http/content_type.rb +27 -0
  22. data/lib/http/errors.rb +13 -0
  23. data/lib/http/headers.rb +154 -0
  24. data/lib/http/headers/mixin.rb +11 -0
  25. data/lib/http/mime_type.rb +61 -36
  26. data/lib/http/mime_type/adapter.rb +24 -0
  27. data/lib/http/mime_type/json.rb +23 -0
  28. data/lib/http/options.rb +21 -48
  29. data/lib/http/redirector.rb +12 -7
  30. data/lib/http/request.rb +82 -33
  31. data/lib/http/request/writer.rb +79 -0
  32. data/lib/http/response.rb +39 -68
  33. data/lib/http/response/body.rb +62 -0
  34. data/lib/http/{response_parser.rb → response/parser.rb} +3 -1
  35. data/lib/http/version.rb +1 -1
  36. data/logo.png +0 -0
  37. data/spec/http/authorization_header/basic_auth_spec.rb +29 -0
  38. data/spec/http/authorization_header/bearer_token_spec.rb +36 -0
  39. data/spec/http/authorization_header_spec.rb +41 -0
  40. data/spec/http/backports/base64_spec.rb +13 -0
  41. data/spec/http/client_spec.rb +181 -0
  42. data/spec/http/content_type_spec.rb +47 -0
  43. data/spec/http/headers/mixin_spec.rb +36 -0
  44. data/spec/http/headers_spec.rb +417 -0
  45. data/spec/http/options/body_spec.rb +6 -7
  46. data/spec/http/options/form_spec.rb +4 -5
  47. data/spec/http/options/headers_spec.rb +9 -17
  48. data/spec/http/options/json_spec.rb +17 -0
  49. data/spec/http/options/merge_spec.rb +18 -19
  50. data/spec/http/options/new_spec.rb +5 -19
  51. data/spec/http/options/proxy_spec.rb +6 -6
  52. data/spec/http/options_spec.rb +3 -9
  53. data/spec/http/redirector_spec.rb +100 -0
  54. data/spec/http/request/writer_spec.rb +25 -0
  55. data/spec/http/request_spec.rb +54 -14
  56. data/spec/http/response/body_spec.rb +24 -0
  57. data/spec/http/response_spec.rb +61 -32
  58. data/spec/http_spec.rb +77 -86
  59. data/spec/spec_helper.rb +25 -2
  60. data/spec/support/example_server.rb +58 -49
  61. data/spec/support/proxy_server.rb +27 -11
  62. metadata +60 -55
  63. data/lib/http/header.rb +0 -11
  64. data/lib/http/mime_types/json.rb +0 -19
  65. data/lib/http/request_stream.rb +0 -77
  66. data/spec/http/options/callbacks_spec.rb +0 -62
  67. data/spec/http/options/response_spec.rb +0 -24
  68. 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
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'forwardable'
2
+
3
+ module HTTP
4
+ class Headers
5
+ module Mixin
6
+ extend Forwardable
7
+ attr_reader :headers
8
+ def_delegators :headers, :[], :[]=
9
+ end
10
+ end
11
+ end
@@ -1,51 +1,76 @@
1
1
  module HTTP
2
- # Yes, HTTP bundles its own MIME type library. Maybe it should be spun off
3
- # as a separate gem or something.
4
- class MimeType
5
- @mime_types, @shortcuts = {}, {}
6
-
2
+ # MIME type encode/decode adapters
3
+ module MimeType
7
4
  class << self
8
- def register(obj)
9
- @mime_types[obj.type] = obj
10
- @shortcuts[obj.shortcut] = obj if obj.shortcut
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
- if type.is_a? Symbol
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
- def initialize(type, shortcut = nil)
25
- @type, @shortcut = type, shortcut
26
- @parse_with = @emit_with = nil
27
-
28
- self.class.register self
29
- end
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
- # Define
32
- def parse_with(&block)
33
- @parse_with = block
34
- end
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
- def emit_with(&block)
37
- @emit_with = block
38
- end
60
+ private
39
61
 
40
- def parse(obj)
41
- @parse_with ? @parse_with[obj] : obj
42
- end
62
+ # :nodoc:
63
+ def adapters
64
+ @adapters ||= {}
65
+ end
43
66
 
44
- def emit(obj)
45
- @emit_with ? @emit_with[obj] : obj
67
+ # :nodoc:
68
+ def aliases
69
+ @aliases ||= {}
70
+ end
46
71
  end
47
72
  end
48
73
  end
49
74
 
50
- # MIME type registry
51
- require 'http/mime_types/json'
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/version'
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=, :callbacks=, :follow=
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 = options[: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.to_hash)
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 with_body(body)
91
+ def with_json(data)
106
92
  dup do |opts|
107
- opts.body = body
93
+ opts.json = data
108
94
  end
109
95
  end
110
96
 
111
- def with_follow(follow)
97
+ def with_body(body)
112
98
  dup do |opts|
113
- opts.follow = follow
99
+ opts.body = body
114
100
  end
115
101
  end
116
102
 
117
- def with_callback(event, callback)
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.callbacks = callbacks.dup
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
- private
152
+ private
179
153
 
180
154
  def argument_error!(message)
181
- raise ArgumentError, message, caller[1..-1]
155
+ fail(Error, message, caller[1..-1])
182
156
  end
183
-
184
157
  end
185
158
  end