resourceful 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  require 'net/http'
2
2
 
3
- require 'resourceful/options_interpreter'
3
+ require 'resourceful/options_interpretation'
4
4
  require 'resourceful/authentication_manager'
5
5
  require 'resourceful/cache_manager'
6
6
  require 'resourceful/resource'
@@ -28,7 +28,8 @@ module Resourceful
28
28
  # provided by the Resourceful library. Conceptually this object
29
29
  # acts a collection of all the resources available via HTTP.
30
30
  class HttpAccessor
31
-
31
+ include OptionsInterpretation
32
+
32
33
  # A logger object to which messages about the activities of this
33
34
  # object will be written. This should be an object that responds
34
35
  # to +#info(message)+ and +#debug(message)+.
@@ -40,40 +41,45 @@ module Resourceful
40
41
  attr_reader :auth_manager
41
42
  attr_reader :user_agent_tokens
42
43
 
43
- INIT_OPTIONS = OptionsInterpreter.new do
44
- option(:logger, :default => Resourceful::BitBucketLogger.new)
45
- option(:user_agent, :default => []) {|ua| [ua].flatten}
46
- option(:cache_manager, :default => NullCacheManager.new)
47
- option(:authenticator)
48
- option(:authenticators, :default => [])
49
- end
44
+ ##
45
+ # The adapter this accessor will use to make the actual HTTP requests.
46
+ attr_reader :http_adapter
50
47
 
51
48
  # Initializes a new HttpAccessor. Valid options:
52
49
  #
53
- # +:logger+:: A Logger object that the new HTTP accessor should
54
- # send log messages
50
+ # `:logger`
51
+ # : A Logger object that the new HTTP accessor should send log messages
52
+ #
53
+ # `:user_agent`
54
+ # : One or more additional user agent tokens to added to the user agent string.
55
55
  #
56
- # +:user_agent+:: One or more additional user agent tokens to
57
- # added to the user agent string.
56
+ # `:cache_manager`
57
+ # : The cache manager this accessor should use.
58
58
  #
59
- # +:cache_manager+:: The cache manager this accessor should use.
59
+ # `:authenticator`
60
+ # : Add a single authenticator for this accessor.
60
61
  #
61
- # +:authenticator+:: Add a single authenticator for this accessor.
62
+ # `:authenticators`
63
+ # : Enumerable of the authenticators for this accessor.
62
64
  #
63
- # +:authenticators+:: Enumerable of the authenticators for this
64
- # accessor.
65
+ # `http_adapter`
66
+ # : The HttpAdapter to be used by this accessor
67
+ #
68
+ #
65
69
  def initialize(options = {})
66
70
  @user_agent_tokens = [RESOURCEFUL_USER_AGENT_TOKEN]
67
-
68
- INIT_OPTIONS.interpret(options) do |opts|
69
- @user_agent_tokens.push(*opts[:user_agent].reverse)
70
- self.logger = opts[:logger]
71
- @auth_manager = AuthenticationManager.new()
72
- @cache_manager = opts[:cache_manager]
71
+ @auth_manager = AuthenticationManager.new()
72
+
73
+ extract_opts(options) do |opts|
74
+ @user_agent_tokens.push(*opts.extract(:user_agent, :default => []) {|ua| [ua].flatten})
75
+
76
+ self.logger = opts.extract(:logger, :default => BitBucketLogger.new)
77
+ @cache_manager = opts.extract(:cache_manager, :default => NullCacheManager.new)
78
+ @http_adapter = opts.extract(:http_adapter, :default => lambda{NetHttpAdapter.new})
73
79
 
74
- add_authenticator(opts[:authenticator]) if opts[:authenticator]
75
- opts[:authenticators].each { |a| add_authenticator(a) }
76
- end
80
+ opts.extract(:authenticator, :required => false).tap{|a| add_authenticator(a) if a}
81
+ opts.extract(:authenticators, :default => []).each { |a| add_authenticator(a) }
82
+ end
77
83
  end
78
84
 
79
85
  # Returns the string that identifies this HTTP accessor. If you
@@ -0,0 +1,46 @@
1
+ require 'resourceful/abstract_form_data'
2
+
3
+ module Resourceful
4
+ class MultipartFormData < AbstractFormData
5
+ FileParamValue = Struct.new(:content, :file_name, :content_type)
6
+
7
+ def add_file(name, file_name, content_type="application/octet-stream")
8
+ add(name, FileParamValue.new(File.new(file_name, 'r'), File.basename(file_name), content_type))
9
+ end
10
+
11
+ def content_type
12
+ "multipart/form-data; boundary=#{boundary}"
13
+ end
14
+
15
+ def read
16
+ StringIO.new.tap do |out|
17
+ first = true
18
+ form_data.each do |key, val|
19
+ out << "\r\n" unless first
20
+ out << "--" << boundary
21
+ out << "\r\nContent-Disposition: form-data; name=\"#{key}\""
22
+ if val.kind_of?(FileParamValue)
23
+ out << "; filename=\"#{val.file_name}\""
24
+ out << "\r\nContent-Type: #{val.content_type}"
25
+ end
26
+ out << "\r\n\r\n"
27
+ if val.kind_of?(FileParamValue)
28
+ out << val.content.read
29
+ else
30
+ out << val.to_s
31
+ end
32
+ first = false
33
+ end
34
+ out << "\r\n--#{boundary}--"
35
+ end.string
36
+ end
37
+
38
+ protected
39
+
40
+ def boundary
41
+ @boundary ||= (0..30).map{BOUNDARY_CHARS[rand(BOUNDARY_CHARS.length)]}.join
42
+ end
43
+
44
+ BOUNDARY_CHARS = [('a'..'z').to_a,('A'..'Z').to_a,(0..9).to_a].flatten
45
+ end
46
+ end
@@ -23,17 +23,31 @@ module Resourceful
23
23
  # Make an HTTP request using the standard library net/http.
24
24
  #
25
25
  # Will use a proxy defined in the http_proxy environment variable, if set.
26
- def self.make_request(method, uri, body = nil, header = nil)
26
+ #
27
+ # @param [#read] body
28
+ # An IO-ish thing containing the body of the request
29
+ #
30
+ def make_request(method, uri, body = nil, header = nil)
27
31
  uri = uri.is_a?(Addressable::URI) ? uri : Addressable::URI.parse(uri)
28
32
 
33
+ if [:put, :post].include? method
34
+ body = body ? body.read : ""
35
+ header[:content_length] = body.size
36
+ end
37
+
29
38
  req = net_http_request_class(method).new(uri.absolute_path)
30
39
  header.each_field { |k,v| req[k] = v } if header
31
40
  https = ("https" == uri.scheme)
32
- conn = Net::HTTP.Proxy(*proxy_details).new(uri.host, uri.port || (https ? 443 : 80))
41
+ conn_class = proxy_details ? Net::HTTP.Proxy(*proxy_details) : Net::HTTP
42
+ conn = conn_class.new(uri.host, uri.port || (https ? 443 : 80))
33
43
  conn.use_ssl = https
34
44
  begin
35
45
  conn.start
36
- res = conn.request(req, body)
46
+ res = if body
47
+ conn.request(req, body)
48
+ else
49
+ conn.request(req)
50
+ end
37
51
  ensure
38
52
  conn.finish if conn.started?
39
53
  end
@@ -49,12 +63,12 @@ module Resourceful
49
63
  private
50
64
 
51
65
  # Parse proxy details from http_proxy environment variable
52
- def self.proxy_details
66
+ def proxy_details
53
67
  proxy = Addressable::URI.parse(ENV["http_proxy"])
54
68
  [proxy.host, proxy.port, proxy.user, proxy.password] if proxy
55
69
  end
56
70
 
57
- def self.net_http_request_class(method)
71
+ def net_http_request_class(method)
58
72
  case method
59
73
  when :get then Net::HTTP::Get
60
74
  when :head then Net::HTTP::Head
@@ -0,0 +1,72 @@
1
+ module Resourceful
2
+ # Declarative way of interpreting options hashes
3
+ #
4
+ # include OptionsInterpretion
5
+ # def my_method(opts = {})
6
+ # extract_opts(opts) do |opts|
7
+ # host = opts.extract(:host)
8
+ # port = opts.extract(:port, :default => 80) {|p| Integer(p)}
9
+ # end
10
+ # end
11
+ #
12
+ module OptionsInterpretation
13
+ # Interpret an options hash
14
+ #
15
+ # @param [Hash] opts
16
+ # The options to interpret.
17
+ #
18
+ # @yield block that used to interpreter options hash
19
+ #
20
+ # @yieldparam [Resourceful::OptionsInterpretion::OptionsInterpreter] interpeter
21
+ # An interpreter that can be used to extract option information from the options hash.
22
+ def extract_opts(opts, &blk)
23
+ opts = opts.clone
24
+ yield OptionsInterpreter.new(opts)
25
+
26
+ unless opts.empty?
27
+ raise ArgumentError, "Unrecognized options: #{opts.keys.join(", ")}"
28
+ end
29
+
30
+ end
31
+
32
+ class OptionsInterpreter
33
+ def initialize(options_hash)
34
+ @options_hash = options_hash
35
+ end
36
+
37
+ # Extract a particular option.
38
+ #
39
+ # @param [String] name
40
+ # Name of option to extract
41
+ # @param [Hash] interpreter_opts
42
+ # ':default'
43
+ # :: The default value, or an object that responds to #call
44
+ # with the default value.
45
+ # ':required'
46
+ # :: Boolean indicating if this option is required. Default:
47
+ # false if a default is provided; otherwise true.
48
+ def extract(name, interpreter_opts = {}, &blk)
49
+ option_required = !interpreter_opts.has_key?(:default)
50
+ option_required = interpreter_opts[:required] if interpreter_opts.has_key?(:required)
51
+
52
+ raise ArgumentError, "Required option #{name} not provided" if option_required && !@options_hash.has_key?(name)
53
+ # We have the option we need
54
+
55
+ orig_val = @options_hash.delete(name)
56
+
57
+ if block_given?
58
+ yield orig_val
59
+
60
+ elsif orig_val
61
+ orig_val
62
+
63
+ elsif interpreter_opts[:default] && interpreter_opts[:default].respond_to?(:call)
64
+ interpreter_opts[:default].call()
65
+
66
+ elsif interpreter_opts[:default]
67
+ interpreter_opts[:default]
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -163,7 +163,7 @@ module Resourceful
163
163
  def perform!
164
164
  @request_time = Time.now
165
165
 
166
- http_resp = NetHttpAdapter.make_request(@method, @resource.uri, @body, @header)
166
+ http_resp = adapter.make_request(@method, @resource.uri, @body, @header)
167
167
  @response = Resourceful::Response.new(uri, *http_resp)
168
168
  @response.request_time = @request_time
169
169
  @response.authoritative = true
@@ -205,7 +205,7 @@ module Resourceful
205
205
 
206
206
  # Does this request force us to revalidate the cache?
207
207
  def forces_revalidation?
208
- if max_age == 0 || header.cache_control && cc.include?('no-cache')
208
+ if max_age == 0 || skip_cache?
209
209
  logger.info(" Client forced revalidation")
210
210
  true
211
211
  else
@@ -225,6 +225,10 @@ module Resourceful
225
225
  def logger
226
226
  resource.logger
227
227
  end
228
+
229
+ def adapter
230
+ accessor.http_adapter
231
+ end
228
232
  end
229
233
 
230
234
  end
@@ -5,7 +5,6 @@ module Resourceful
5
5
 
6
6
  class Resource
7
7
  attr_reader :accessor
8
- attr_accessor :default_header
9
8
 
10
9
  # Build a new resource for a uri
11
10
  #
@@ -30,6 +29,10 @@ module Resourceful
30
29
  end
31
30
  alias uri effective_uri
32
31
 
32
+ def default_header(temp_defaults = {})
33
+ @default_header.merge(temp_defaults)
34
+ end
35
+
33
36
  # Returns the host for this Resource's current uri
34
37
  def host
35
38
  Addressable::URI.parse(uri).host
@@ -95,7 +98,6 @@ module Resourceful
95
98
  # @raise [UnsuccessfulHttpRequestError] unless the request is a
96
99
  # success, ie the final request returned a 2xx response code
97
100
  def post(data = nil, header = {})
98
- check_content_type_exists(data, header)
99
101
  request(:post, data, header)
100
102
  end
101
103
 
@@ -116,7 +118,6 @@ module Resourceful
116
118
  # @raise [UnsuccessfulHttpRequestError] unless the request is a
117
119
  # success, ie the final request returned a 2xx response code
118
120
  def put(data, header = {})
119
- check_content_type_exists(data, header)
120
121
  request(:put, data, header)
121
122
  end
122
123
 
@@ -137,17 +138,32 @@ module Resourceful
137
138
  private
138
139
 
139
140
  # Ensures that the request has a content type header
140
- # TODO Move this to request
141
- def check_content_type_exists(body, header)
142
- if body
143
- raise MissingContentType unless header.has_key?(:content_type) or default_header.has_key?(:content_type)
141
+ def ensure_content_type(body, header)
142
+ return if header.has_key?('Content-Type')
143
+
144
+ if body.respond_to?(:content_type)
145
+ header['Content-Type'] = body.content_type
146
+ return
144
147
  end
148
+
149
+ return if default_header.has_key?('Content-Type')
150
+
151
+ # could not figure it out
152
+ raise MissingContentType
145
153
  end
146
154
 
147
155
  # Actually make the request
148
156
  def request(method, data, header)
149
- log_request_with_time "#{method.to_s.upcase} [#{uri}]" do
150
- request = Request.new(method, self, data, default_header.merge(header))
157
+ header = default_header.merge(header)
158
+ ensure_content_type(data, header) if data
159
+
160
+ data = StringIO.new(data) if data.kind_of?(String)
161
+
162
+ logger.debug { header.map {|k,v| "#{k}: #{v}"}.join("\n\t\t") }
163
+ logger.debug { data = StringIO.new(data.read); data.string } if data
164
+
165
+ log_request_with_time "#{method.to_s.upcase} [#{uri}]" do
166
+ request = Request.new(method, self, data, header)
151
167
  request.fetch_response
152
168
  end
153
169
  end
@@ -26,8 +26,8 @@ module Resourceful
26
26
  if header['Cache-Control'] and header['Cache-Control'].first.include?('max-age')
27
27
  max_age = header['Cache-Control'].first.split(',').grep(/max-age/).first.split('=').last.to_i
28
28
  return true if current_age > max_age
29
- elsif header['Expire']
30
- return true if Time.httpdate(header['Expire'].first) < Time.now
29
+ elsif header['Expires']
30
+ return true if Time.httpdate(header['Expires']) < Time.now
31
31
  end
32
32
 
33
33
  false
@@ -75,8 +75,8 @@ module Resourceful
75
75
 
76
76
  # Algorithm taken from RCF2616#13.2.3
77
77
  def current_age
78
- age_value = header['Age'] ? header['Age'].first.to_i : 0
79
- date_value = Time.httpdate(header['Date'].first)
78
+ age_value = header['Age'] || 0
79
+ date_value = Time.httpdate(header['Date'])
80
80
  now = Time.now
81
81
 
82
82
  apparent_age = [0, response_time - date_value].max
@@ -85,7 +85,7 @@ module Resourceful
85
85
  end
86
86
 
87
87
  def body
88
- encoding = header['Content-Encoding'] && header['Content-Encoding'].first
88
+ encoding = header['Content-Encoding']
89
89
  case encoding
90
90
  when nil
91
91
  # body is identity encoded; just return it
@@ -0,0 +1,17 @@
1
+ require 'resourceful/abstract_form_data'
2
+ require 'cgi'
3
+
4
+ module Resourceful
5
+ class UrlencodedFormData < AbstractFormData
6
+
7
+ def content_type
8
+ "application/x-www-form-urlencoded"
9
+ end
10
+
11
+ def read
12
+ @form_data.map do |k,v|
13
+ CGI.escape(k) + '=' + CGI.escape(v)
14
+ end.join('&')
15
+ end
16
+ end
17
+ end
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{resourceful}
5
- s.version = "0.5.4"
5
+ s.version = "0.6.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Paul Sadauskas"]
9
- s.date = %q{2009-08-11}
9
+ s.date = %q{2009-08-14}
10
10
  s.description = %q{An HTTP library for Ruby that takes advantage of everything HTTP has to offer.}
11
11
  s.email = %q{psadauskas@gmail.com}
12
- s.extra_rdoc_files = ["lib/resourceful.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/header.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/response.rb", "lib/resourceful/util.rb", "lib/resourceful/options_interpreter.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/authentication_manager.rb", "README.markdown"]
13
- s.files = ["lib/resourceful.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/header.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/response.rb", "lib/resourceful/util.rb", "lib/resourceful/options_interpreter.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/authentication_manager.rb", "README.markdown", "MIT-LICENSE", "Rakefile", "Manifest", "spec/simple_sinatra_server_spec.rb", "spec/old_acceptance_specs.rb", "spec/acceptance_shared_specs.rb", "spec/spec_helper.rb", "spec/simple_sinatra_server.rb", "spec/acceptance/authorization_spec.rb", "spec/acceptance/header_spec.rb", "spec/acceptance/resource_spec.rb", "spec/acceptance/caching_spec.rb", "spec/acceptance/redirecting_spec.rb", "spec/spec.opts", "resourceful.gemspec"]
12
+ s.extra_rdoc_files = ["lib/resourceful.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/options_interpretation.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/urlencoded_form_data.rb", "lib/resourceful/header.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/response.rb", "lib/resourceful/util.rb", "lib/resourceful/abstract_form_data.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/multipart_form_data.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/authentication_manager.rb", "README.markdown"]
13
+ s.files = ["lib/resourceful.rb", "lib/resourceful/net_http_adapter.rb", "lib/resourceful/options_interpretation.rb", "lib/resourceful/stubbed_resource_proxy.rb", "lib/resourceful/urlencoded_form_data.rb", "lib/resourceful/header.rb", "lib/resourceful/memcache_cache_manager.rb", "lib/resourceful/response.rb", "lib/resourceful/util.rb", "lib/resourceful/abstract_form_data.rb", "lib/resourceful/cache_manager.rb", "lib/resourceful/request.rb", "lib/resourceful/resource.rb", "lib/resourceful/exceptions.rb", "lib/resourceful/multipart_form_data.rb", "lib/resourceful/http_accessor.rb", "lib/resourceful/authentication_manager.rb", "History.txt", "resourceful.gemspec", "README.markdown", "MIT-LICENSE", "Rakefile", "Manifest", "spec/simple_sinatra_server_spec.rb", "spec/old_acceptance_specs.rb", "spec/acceptance_shared_specs.rb", "spec/spec_helper.rb", "spec/simple_sinatra_server.rb", "spec/acceptance/authorization_spec.rb", "spec/acceptance/header_spec.rb", "spec/acceptance/resource_spec.rb", "spec/acceptance/caching_spec.rb", "spec/acceptance/redirecting_spec.rb", "spec/resourceful/multipart_form_data_spec.rb", "spec/resourceful/header_spec.rb", "spec/resourceful/resource_spec.rb", "spec/resourceful/urlencoded_form_data_spec.rb", "spec/caching_spec.rb", "spec/spec.opts"]
14
14
  s.homepage = %q{http://github.com/paul/resourceful}
15
15
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Resourceful", "--main", "README.markdown"]
16
16
  s.require_paths = ["lib"]
@@ -23,14 +23,14 @@ Gem::Specification.new do |s|
23
23
  s.specification_version = 3
24
24
 
25
25
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
- s.add_runtime_dependency(%q<addressable>, [">= 0"])
26
+ s.add_runtime_dependency(%q<addressable>, [">= 2.1.0"])
27
27
  s.add_runtime_dependency(%q<httpauth>, [">= 0"])
28
28
  s.add_development_dependency(%q<thin>, [">= 0"])
29
29
  s.add_development_dependency(%q<yard>, [">= 0"])
30
30
  s.add_development_dependency(%q<sinatra>, [">= 0"])
31
31
  s.add_development_dependency(%q<rspec>, [">= 0"])
32
32
  else
33
- s.add_dependency(%q<addressable>, [">= 0"])
33
+ s.add_dependency(%q<addressable>, [">= 2.1.0"])
34
34
  s.add_dependency(%q<httpauth>, [">= 0"])
35
35
  s.add_dependency(%q<thin>, [">= 0"])
36
36
  s.add_dependency(%q<yard>, [">= 0"])
@@ -38,7 +38,7 @@ Gem::Specification.new do |s|
38
38
  s.add_dependency(%q<rspec>, [">= 0"])
39
39
  end
40
40
  else
41
- s.add_dependency(%q<addressable>, [">= 0"])
41
+ s.add_dependency(%q<addressable>, [">= 2.1.0"])
42
42
  s.add_dependency(%q<httpauth>, [">= 0"])
43
43
  s.add_dependency(%q<thin>, [">= 0"])
44
44
  s.add_dependency(%q<yard>, [">= 0"])