cryx-rest-client 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ module RestClient
2
+ # A class that can be instantiated for access to a RESTful resource,
3
+ # including authentication.
4
+ #
5
+ # Example:
6
+ #
7
+ # resource = RestClient::Resource.new('http://some/resource')
8
+ # jpg = resource.get(:accept => 'image/jpg')
9
+ #
10
+ # With HTTP basic authentication:
11
+ #
12
+ # resource = RestClient::Resource.new('http://protected/resource', :user => 'user', :password => 'password')
13
+ # resource.delete
14
+ #
15
+ # With a timeout (seconds):
16
+ #
17
+ # RestClient::Resource.new('http://slow', :timeout => 10)
18
+ #
19
+ # With an open timeout (seconds):
20
+ #
21
+ # RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
22
+ #
23
+ # You can also use resources to share common headers. For headers keys,
24
+ # symbols are converted to strings. Example:
25
+ #
26
+ # resource = RestClient::Resource.new('http://some/resource', :headers => { :client_version => 1 })
27
+ #
28
+ # This header will be transported as X-Client-Version (notice the X prefix,
29
+ # capitalization and hyphens)
30
+ #
31
+ # Use the [] syntax to allocate subresources:
32
+ #
33
+ # site = RestClient::Resource.new('http://example.com', :user => 'adam', :password => 'mypasswd')
34
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
35
+ #
36
+ class Resource
37
+ attr_reader :url, :options
38
+
39
+ def initialize(url, options={}, backwards_compatibility=nil)
40
+ @url = url
41
+ if options.class == Hash
42
+ @options = options
43
+ else # compatibility with previous versions
44
+ @options = { :user => options, :password => backwards_compatibility }
45
+ end
46
+ end
47
+
48
+ def get(additional_headers={})
49
+ Request.execute(options.merge(
50
+ :method => :get,
51
+ :url => url,
52
+ :headers => headers.merge(additional_headers)
53
+ ))
54
+ end
55
+
56
+ def post(payload, additional_headers={})
57
+ Request.execute(options.merge(
58
+ :method => :post,
59
+ :url => url,
60
+ :payload => payload,
61
+ :headers => headers.merge(additional_headers)
62
+ ))
63
+ end
64
+
65
+ def put(payload, additional_headers={})
66
+ Request.execute(options.merge(
67
+ :method => :put,
68
+ :url => url,
69
+ :payload => payload,
70
+ :headers => headers.merge(additional_headers)
71
+ ))
72
+ end
73
+
74
+ def delete(additional_headers={})
75
+ Request.execute(options.merge(
76
+ :method => :delete,
77
+ :url => url,
78
+ :headers => headers.merge(additional_headers)
79
+ ))
80
+ end
81
+
82
+ def to_s
83
+ url
84
+ end
85
+
86
+ def user
87
+ options[:user]
88
+ end
89
+
90
+ def password
91
+ options[:password]
92
+ end
93
+
94
+ def headers
95
+ options[:headers] || {}
96
+ end
97
+
98
+ def timeout
99
+ options[:timeout]
100
+ end
101
+
102
+ def open_timeout
103
+ options[:open_timeout]
104
+ end
105
+
106
+ # Construct a subresource, preserving authentication.
107
+ #
108
+ # Example:
109
+ #
110
+ # site = RestClient::Resource.new('http://example.com', 'adam', 'mypasswd')
111
+ # site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
112
+ #
113
+ # This is especially useful if you wish to define your site in one place and
114
+ # call it in multiple locations:
115
+ #
116
+ # def orders
117
+ # RestClient::Resource.new('http://example.com/orders', 'admin', 'mypasswd')
118
+ # end
119
+ #
120
+ # orders.get # GET http://example.com/orders
121
+ # orders['1'].get # GET http://example.com/orders/1
122
+ # orders['1/items'].delete # DELETE http://example.com/orders/1/items
123
+ #
124
+ # Nest resources as far as you want:
125
+ #
126
+ # site = RestClient::Resource.new('http://example.com')
127
+ # posts = site['posts']
128
+ # first_post = posts['1']
129
+ # comments = first_post['comments']
130
+ # comments.post 'Hello', :content_type => 'text/plain'
131
+ #
132
+ def [](suburl)
133
+ self.class.new(concat_urls(url, suburl), options)
134
+ end
135
+
136
+ def concat_urls(url, suburl) # :nodoc:
137
+ url = url.to_s
138
+ suburl = suburl.to_s
139
+ if url.slice(-1, 1) == '/' or suburl.slice(0, 1) == '/'
140
+ url + suburl
141
+ else
142
+ "#{url}/#{suburl}"
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,93 @@
1
+ module RestClient
2
+ # The response from RestClient looks like a string, but is actually one of
3
+ # these. 99% of the time you're making a rest call all you care about is
4
+ # the body, but on the occassion you want to fetch the headers you can:
5
+ #
6
+ # RestClient.get('http://example.com').headers[:content_type]
7
+ #
8
+ class Response < String
9
+ include Logging
10
+ attr_reader :net_http_res
11
+
12
+ # the headers that makes a response cacheable
13
+ REQUIRED_HEADERS = [
14
+ [:date, :last_modified], [:date, :expires_value], [:date, :cache_control], [:etag]
15
+ ].freeze
16
+
17
+ REGEXP = {
18
+ :max_age => /max-age\s?=\s?(\d+)/
19
+ }.freeze
20
+
21
+ def initialize(string, net_http_res)
22
+ @net_http_res = net_http_res
23
+ super(string || "")
24
+ end
25
+
26
+ # HTTP status code, always 200 since RestClient throws exceptions for
27
+ # other codes.
28
+ def code
29
+ @code ||= @net_http_res.code.to_i
30
+ end
31
+
32
+ # A hash of the headers, beautified with symbols and underscores.
33
+ # e.g. "Content-type" will become :content_type.
34
+ def headers
35
+ @headers ||= self.class.beautify_headers(@net_http_res.to_hash)
36
+ end
37
+
38
+ # Hash of cookies extracted from response headers
39
+ def cookies
40
+ @cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
41
+ key, val = raw_c.split('=')
42
+ unless %w(expires domain path secure).member?(key)
43
+ out[key] = val
44
+ end
45
+ out
46
+ end
47
+ end
48
+
49
+ def self.beautify_headers(headers)
50
+ headers.inject({}) do |out, (key, value)|
51
+ out[key.gsub(/-/, '_').to_sym] = value.first
52
+ out
53
+ end
54
+ end
55
+
56
+ # Checks if the response is still fresh by comparing its current age to the max allowed age or the expires value.
57
+ # http://tools.ietf.org/html/rfc2616#section-13.2.3
58
+ def fresh?(options = {})
59
+ date_value = Time.parse(headers[:date]) rescue nil
60
+ return false if date_value.nil?
61
+ local_response_time = headers[:local_response_time] || date_value
62
+ local_request_time = headers[:local_request_time] || date_value
63
+ age_value = headers[:age].to_i rescue 0
64
+ expires_value = Time.parse(headers[:expires]) rescue nil
65
+ max_age_value = headers[:cache_control].scan(REGEXP[:max_age]).flatten[0].to_i rescue nil
66
+ user_max_age = options[:cache_control].scan(REGEXP[:max_age]).flatten[0].to_i rescue nil
67
+ # age calculation
68
+ apparent_age = [0, local_response_time - date_value].max
69
+ corrected_received_age = [apparent_age, age_value].max
70
+ response_delay = local_response_time - local_request_time;
71
+ corrected_initial_age = corrected_received_age + response_delay;
72
+ resident_time = Time.now - local_response_time;
73
+ current_age = corrected_initial_age + resident_time;
74
+ max_age = max_age_value ? (user_max_age ? [max_age_value, user_max_age].min : max_age_value) : (user_max_age ? user_max_age : nil)
75
+ if max_age then current_age < max_age
76
+ elsif expires_value then current_age < (expires_value - date_value)
77
+ else false; end
78
+ end
79
+
80
+ # Checks if the response is cacheable by comparing its headers to the set of headers that make a response cacheable.
81
+ # See the REQUIRED_HEADERS constant for more information
82
+ def cacheable?
83
+ REQUIRED_HEADERS.each do |required_headers|
84
+ available_headers = headers.keys & required_headers
85
+ has_required_headers = available_headers.length == required_headers.length
86
+ required_headers_are_not_empty = !headers.values_at(*available_headers).map{|value| value.nil? || value.empty?}.include?(true)
87
+ return true if has_required_headers && required_headers_are_not_empty
88
+ end
89
+ display_log "# [cache] the response is not cacheable"
90
+ false
91
+ end
92
+ end
93
+ end
data/lib/restclient.rb ADDED
@@ -0,0 +1,95 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+ require 'zlib'
4
+ require 'stringio'
5
+ require 'time'
6
+
7
+ require File.dirname(__FILE__) + '/restclient/logging'
8
+ require File.dirname(__FILE__) + '/restclient/request'
9
+ require File.dirname(__FILE__) + '/restclient/response'
10
+ require File.dirname(__FILE__) + '/restclient/resource'
11
+ require File.dirname(__FILE__) + '/restclient/cacheable_resource'
12
+ require File.dirname(__FILE__) + '/restclient/exceptions'
13
+
14
+
15
+ # This module's static methods are the entry point for using the REST client.
16
+ #
17
+ # # GET
18
+ # xml = RestClient.get 'http://example.com/resource'
19
+ # jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
20
+ #
21
+ # # authentication and SSL
22
+ # RestClient.get 'https://user:password@example.com/private/resource'
23
+ #
24
+ # # POST or PUT with a hash sends parameters as a urlencoded form body
25
+ # RestClient.post 'http://example.com/resource', :param1 => 'one'
26
+ #
27
+ # # nest hash parameters
28
+ # RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
29
+ #
30
+ # # POST and PUT with raw payloads
31
+ # RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
32
+ # RestClient.post 'http://example.com/resource.xml', xml_doc
33
+ # RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
34
+ #
35
+ # # DELETE
36
+ # RestClient.delete 'http://example.com/resource'
37
+ #
38
+ # # retreive the response http code and headers
39
+ # res = RestClient.get 'http://example.com/some.jpg'
40
+ # res.code # => 200
41
+ # res.headers[:content_type] # => 'image/jpg'
42
+ #
43
+ # # HEAD
44
+ # RestClient.head('http://example.com').headers
45
+ #
46
+ # To use with a proxy, just set RestClient.proxy to the proper http proxy:
47
+ #
48
+ # RestClient.proxy = "http://proxy.example.com/"
49
+ #
50
+ # Or inherit the proxy from the environment:
51
+ #
52
+ # RestClient.proxy = ENV['http_proxy']
53
+ #
54
+ # For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
55
+ #
56
+ # >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
57
+ # => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
58
+ #
59
+ module RestClient
60
+ def self.get(url, headers={})
61
+ Request.execute(:method => :get, :url => url, :headers => headers)
62
+ end
63
+
64
+ def self.post(url, payload, headers={})
65
+ Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers)
66
+ end
67
+
68
+ def self.put(url, payload, headers={})
69
+ Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers)
70
+ end
71
+
72
+ def self.delete(url, headers={})
73
+ Request.execute(:method => :delete, :url => url, :headers => headers)
74
+ end
75
+
76
+ def self.head(url, headers={})
77
+ Request.execute(:method => :head, :url => url, :headers => headers)
78
+ end
79
+
80
+ class << self
81
+ attr_accessor :proxy
82
+ end
83
+
84
+ # Print log of RestClient calls. Value can be stdout, stderr, or a filename.
85
+ # You can also configure logging by the environment variable RESTCLIENT_LOG.
86
+ def self.log=(log)
87
+ @@log = log
88
+ end
89
+
90
+ def self.log # :nodoc:
91
+ return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
92
+ return @@log if defined? @@log
93
+ nil
94
+ end
95
+ end
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "rest-client"
3
+ s.version = "0.9.1"
4
+ s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
5
+ s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
6
+ s.author = "Adam Wiggins"
7
+ s.email = "adam@heroku.com"
8
+ s.rubyforge_project = "rest-client"
9
+ s.homepage = "http://rest-client.heroku.com/"
10
+ s.has_rdoc = true
11
+ s.platform = Gem::Platform::RUBY
12
+ s.files = %w(Rakefile README.rdoc rest-client.gemspec
13
+ lib/rest_client.rb lib/restclient.rb
14
+ lib/restclient/request.rb lib/restclient/response.rb
15
+ lib/restclient/exceptions.rb lib/restclient/resource.rb
16
+ spec/base.rb spec/request_spec.rb spec/response_spec.rb
17
+ spec/exceptions_spec.rb spec/resource_spec.rb spec/restclient_spec.rb
18
+ bin/restclient)
19
+ s.executables = ['restclient']
20
+ s.require_path = "lib"
21
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+
4
+ require File.dirname(__FILE__) + '/../lib/restclient'
5
+
6
+ def cacheable_headers
7
+ {:etag => '9d265df2cff9bd59703e518d50efcf99457f2085'}
8
+ end
9
+
10
+ def basic_headers
11
+ {:date => Time.now.httpdate}
12
+ end
@@ -0,0 +1,54 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ describe RestClient::Exception do
4
+ it "sets the exception message to ErrorMessage" do
5
+ RestClient::ResourceNotFound.new.message.should == 'Resource not found'
6
+ end
7
+
8
+ it "contains exceptions in RestClient" do
9
+ RestClient::Unauthorized.new.should be_a_kind_of(RestClient::Exception)
10
+ RestClient::ServerBrokeConnection.new.should be_a_kind_of(RestClient::Exception)
11
+ end
12
+ end
13
+
14
+ describe RestClient::RequestFailed do
15
+ it "stores the http response on the exception" do
16
+ begin
17
+ raise RestClient::RequestFailed, :response
18
+ rescue RestClient::RequestFailed => e
19
+ e.response.should == :response
20
+ end
21
+ end
22
+
23
+ it "http_code convenience method for fetching the code as an integer" do
24
+ RestClient::RequestFailed.new(mock('res', :code => '502')).http_code.should == 502
25
+ end
26
+
27
+ it "shows the status code in the message" do
28
+ RestClient::RequestFailed.new(mock('res', :code => '502')).to_s.should match(/502/)
29
+ end
30
+ end
31
+
32
+ describe RestClient::ResourceNotFound do
33
+ it "also has the http response attached" do
34
+ begin
35
+ raise RestClient::ResourceNotFound, :response
36
+ rescue RestClient::ResourceNotFound => e
37
+ e.response.should == :response
38
+ end
39
+ end
40
+ end
41
+
42
+ describe "backwards compatibility" do
43
+ it "alias RestClient::Request::Redirect to RestClient::Redirect" do
44
+ RestClient::Request::Redirect.should == RestClient::Redirect
45
+ end
46
+
47
+ it "alias RestClient::Request::Unauthorized to RestClient::Unauthorized" do
48
+ RestClient::Request::Unauthorized.should == RestClient::Unauthorized
49
+ end
50
+
51
+ it "alias RestClient::Request::RequestFailed to RestClient::RequestFailed" do
52
+ RestClient::Request::RequestFailed.should == RestClient::RequestFailed
53
+ end
54
+ end