├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.rdoc ├── Rakefile ├── VERSION ├── examples ├── beautify_html.rb ├── caching.rb └── parsing.rb ├── lib └── restclient │ └── components.rb ├── rest-client-components.gemspec └── spec ├── components_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.swp 3 | *.gem 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.5.0 2 | 3 | * Compatibility with RestClient 2.0 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :test, :development do 6 | gem 'rake' 7 | gem 'rack-cache' 8 | end 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Cyril Rohr 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = rest-client-components 2 | 3 | RestClient on steroids ! 4 | 5 | Want to add transparent HTTP caching to the rest-client[http://github.com/archiloque/rest-client] gem ? It's as simple as: 6 | require 'restclient/components' 7 | require 'rack/cache' 8 | RestClient.enable Rack::Cache 9 | RestClient.get "http://some/cacheable/resource" 10 | 11 | Want to log the requests in the commonlog format ? 12 | require 'restclient/components' 13 | RestClient.enable Rack::CommonLogger, STDOUT 14 | RestClient.get "http://some/resource" 15 | 16 | Want to enable both ? 17 | require 'restclient/components' 18 | require 'rack/cache' 19 | RestClient.enable Rack::CommonLogger, STDOUT 20 | RestClient.enable Rack::Cache 21 | RestClient.get "http://some/cacheable/resource" 22 | 23 | This works with any Rack middleware, thus you can reuse the wide range of existing Rack middleware to add functionalities to RestClient with very little effort. 24 | The order in which you enable components will be respected. 25 | 26 | Note that the rest-client behaviour is also respected: you'll get back a RestClient::Response or RestClient exceptions as a result of your requests. If you prefer a more rack-esque approach, just disable the Compatibility component, and you will get back a response conform to the Rack SPEC: 27 | require 'restclient/components' 28 | RestClient.disable RestClient::Rack::Compatibility 29 | status, header, body = RestClient.get('http://some/url') 30 | In that case, you will only get exceptions for connection or timeout errors (RestClient::ServerBrokeConnection or RestClient::RequestTimeout) 31 | 32 | See the examples folder for more details. 33 | 34 | = Installation 35 | 36 | gem install rest-client-components 37 | gem install rack-cache # if you want to use Rack::Cache 38 | 39 | = Usage 40 | Example with Rack::Cache, and Rack::CommonLogger 41 | 42 | require 'restclient/components' 43 | require 'rack/cache' 44 | 45 | RestClient.enable Rack::CommonLogger 46 | # Enable the cache Rack middleware, and store both meta and entity data in files: 47 | # See http://rtomayko.github.io/rack-cache/configuration for the list of available options 48 | RestClient.enable Rack::Cache, 49 | :metastore => 'file:/tmp/cache/meta', 50 | :entitystore => 'file:/tmp/cache/body' 51 | 52 | 53 | # ... done ! 54 | # Then you can make your requests as usual: 55 | # You'll get a log for each call, and the resources will be automatically and transparently cached for you according to their HTTP headers. 56 | # Cache invalidation on requests other than GET is also transparently supported, thanks to Rack::Cache. Enjoy ! 57 | 58 | RestClient.get 'http://some/cacheable/resource' 59 | # or 60 | resource = RestClient::Resource.new('http://some/cacheable/resource') 61 | 62 | # obviously, caching is only interesting if you request the same resource multiple times, e.g. : 63 | resource.get # get from origin server, and cache if possible 64 | resource.get # get from cache, if still fresh. 65 | resource.put(...) # will automatically invalidate the cache, so that a subsequent GET request on the same resource does not return the cached resource 66 | resource.get # get from origin server, and cache if possible 67 | # ... 68 | resource.get(:cache_control => 'no-cache') # explicitly tells to bypass the cache, requires rack-cache >= 0.5 and :allow_reload => true option 69 | # ... 70 | resource.delete(...) # will invalidate the cache 71 | resource.get # should raise a RestClient::ResourceNotFound exception 72 | 73 | 74 | Now, you just need to make your resources cacheable, so unless you've already taken care of that, do yourself a favor and read: 75 | * the HTTP specification related to HTTP caching - http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html 76 | * Things Caches Do - http://tomayko.com/writings/things-caches-do 77 | 78 | = Dependencies 79 | 80 | * rest-client >= 1.4.1 81 | * rack >= 1.0.1 82 | 83 | = COPYRIGHT 84 | 85 | Copyright (c) 2009-2010 Cyril Rohr. See LICENSE for details. 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | begin 4 | require 'jeweler' 5 | Jeweler::Tasks.new do |s| 6 | s.name = "rest-client-components" 7 | s.summary = %Q{RestClient on steroids ! Easily add one or more Rack middleware around RestClient to add functionalities such as transparent caching (Rack::Cache), transparent logging, etc.} 8 | s.email = "cyril.rohr@gmail.com" 9 | s.homepage = "http://github.com/crohr/rest-client-components" 10 | s.description = "RestClient on steroids ! Easily add one or more Rack middleware around RestClient to add functionalities such as transparent caching (Rack::Cache), transparent logging, etc." 11 | s.authors = ["Cyril Rohr"] 12 | s.add_dependency "rest-client", ">= 1.6.0" 13 | s.add_dependency "rack", ">= 1.0.1" 14 | s.add_development_dependency "webmock", ">= 1.21" 15 | s.add_development_dependency "rspec", ">= 3.2.0" 16 | end 17 | rescue LoadError 18 | puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com" 19 | end 20 | 21 | require 'rdoc/task' 22 | Rake::RDocTask.new do |rdoc| 23 | rdoc.rdoc_dir = 'rdoc' 24 | rdoc.title = 'rest-client-components' 25 | rdoc.options << '--line-numbers' << '--inline-source' 26 | rdoc.rdoc_files.include('README*') 27 | rdoc.rdoc_files.include('lib/**/*.rb') 28 | end 29 | 30 | begin 31 | require 'rspec/core/rake_task' 32 | RSpec::Core::RakeTask.new(:spec) 33 | rescue LoadError 34 | end 35 | 36 | task :default => :spec 37 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.5.0 2 | -------------------------------------------------------------------------------- /examples/beautify_html.rb: -------------------------------------------------------------------------------- 1 | # this examples uses Rack::Tidy to automatically tidy the HTML responses returned 2 | require File.dirname(__FILE__) + '/../lib/restclient/components' 3 | require 'rack/tidy' # gem install rack-tidy 4 | 5 | URL = "http://coderack.org/users/webficient/entries/38-racktidy" 6 | puts "Without rack-tidy" 7 | response = RestClient.get URL 8 | puts response 9 | 10 | puts "With rack-tidy" 11 | RestClient.enable Rack::Tidy 12 | 13 | response = RestClient.get URL 14 | puts response 15 | -------------------------------------------------------------------------------- /examples/caching.rb: -------------------------------------------------------------------------------- 1 | require 'restclient/components' 2 | require 'rack/cache' 3 | require 'json' 4 | require 'logger' 5 | require 'time' 6 | require 'digest/sha1' 7 | require 'sinatra/base' 8 | 9 | 10 | def server(base=Sinatra::Base, &block) 11 | app = Sinatra.new(base, &block) 12 | pid = fork do 13 | app.run!(:port => 7890, :host => "localhost") 14 | end 15 | pid 16 | end 17 | 18 | pid = server do 19 | RESOURCE = { 20 | :updated_at => Time.at(Time.now-2), 21 | :content => "hello" 22 | } 23 | get '/cacheable/resource' do 24 | response['Cache-Control'] = "public, max-age=4" 25 | last_modified RESOURCE[:updated_at].httpdate 26 | etag Digest::SHA1.hexdigest(RESOURCE[:content]) 27 | RESOURCE[:content] 28 | end 29 | 30 | put '/cacheable/resource' do 31 | RESOURCE[:content] = params[:content] 32 | RESOURCE[:updated_at] = Time.now 33 | response['Location'] = '/cacheable/resource' 34 | "ok" 35 | end 36 | end 37 | 38 | RestClient.enable Rack::CommonLogger 39 | RestClient.enable Rack::Cache, :verbose => true, :allow_reload => true, :allow_revalidate => true 40 | RestClient.enable Rack::Lint 41 | 42 | begin 43 | puts "Manipulating cacheable resource..." 44 | 6.times do 45 | sleep 1 46 | RestClient.get "http://localhost:7890/cacheable/resource" do |response| 47 | p [response.code, response.headers[:etag], response.headers[:last_modified], response.to_s] 48 | end 49 | end 50 | sleep 1 51 | RestClient.put "http://localhost:7890/cacheable/resource", {:content => "world"} do |response| 52 | p [response.code, response.headers[:etag], response.headers[:last_modified], response.to_s] 53 | end 54 | # note how the cache is automatically invalidated on non-GET requests 55 | 2.times do 56 | sleep 1 57 | RestClient.get "http://localhost:7890/cacheable/resource" do |response| 58 | p [response.code, response.headers[:etag], response.headers[:last_modified], response.to_s] 59 | end 60 | end 61 | rescue RestClient::Exception => e 62 | p [:error, e.message] 63 | ensure 64 | Process.kill("INT", pid) 65 | Process.wait 66 | end 67 | 68 | __END__ 69 | Manipulating cacheable resource... 70 | == Sinatra/0.9.4 has taken the stage on 7890 for development with backup from Thin 71 | >> Thin web server (v1.2.5 codename This Is Not A Web Server) 72 | >> Maximum connections set to 1024 73 | >> Listening on localhost:7890, CTRL+C to stop 74 | cache: [GET /cacheable/resource] miss, store 75 | - - - [15/Feb/2010 22:37:27] "GET /cacheable/resource " 200 5 0.0117 76 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 77 | cache: [GET /cacheable/resource] fresh 78 | - - - [15/Feb/2010 22:37:28] "GET /cacheable/resource " 200 5 0.0022 79 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 80 | cache: [GET /cacheable/resource] fresh 81 | - - - [15/Feb/2010 22:37:29] "GET /cacheable/resource " 200 5 0.0017 82 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 83 | cache: [GET /cacheable/resource] fresh 84 | - - - [15/Feb/2010 22:37:30] "GET /cacheable/resource " 200 5 0.0019 85 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 86 | cache: [GET /cacheable/resource] stale, valid, store 87 | - - - [15/Feb/2010 22:37:31] "GET /cacheable/resource " 200 5 0.0074 88 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 89 | cache: [GET /cacheable/resource] fresh 90 | - - - [15/Feb/2010 22:37:32] "GET /cacheable/resource " 200 5 0.0017 91 | [200, "\"aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d\"", "Mon, 15 Feb 2010 21:37:24 GMT", "hello"] 92 | cache: [PUT /cacheable/resource] invalidate, pass 93 | - - - [15/Feb/2010 22:37:33] "PUT /cacheable/resource " 200 2 0.0068 94 | [200, nil, nil, "ok"] 95 | cache: [GET /cacheable/resource] stale, invalid, store 96 | - - - [15/Feb/2010 22:37:34] "GET /cacheable/resource " 200 5 0.0083 97 | [200, "\"7c211433f02071597741e6ff5a8ea34789abbf43\"", "Mon, 15 Feb 2010 21:37:33 GMT", "world"] 98 | cache: [GET /cacheable/resource] fresh 99 | - - - [15/Feb/2010 22:37:35] "GET /cacheable/resource " 200 5 0.0017 100 | [200, "\"7c211433f02071597741e6ff5a8ea34789abbf43\"", "Mon, 15 Feb 2010 21:37:33 GMT", "world"] 101 | >> Stopping ... 102 | 103 | == Sinatra has ended his set (crowd applauds) 104 | -------------------------------------------------------------------------------- /examples/parsing.rb: -------------------------------------------------------------------------------- 1 | # In this example, we automatically parse the response body if the Content-Type looks like JSON 2 | require File.dirname(__FILE__) + '/../lib/restclient/components' 3 | require 'json' 4 | 5 | module Rack 6 | class JSON 7 | def initialize app 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | status, header, body = @app.call env 13 | content = "" 14 | body.each{|line| content << line} 15 | parsed_body = ::JSON.parse content if header['Content-Type'] =~ /^application\/.*json/i 16 | [status, header, parsed_body] 17 | end 18 | end 19 | end 20 | 21 | RestClient.disable RestClient::Rack::Compatibility 22 | # this breaks the Rack spec, but it should be the last component to be enabled. 23 | RestClient.enable Rack::JSON 24 | 25 | status, header, parsed_body = RestClient.get "http://twitter.com/statuses/user_timeline/20191563.json" 26 | p parsed_body.map{|tweet| tweet['text']} 27 | -------------------------------------------------------------------------------- /lib/restclient/components.rb: -------------------------------------------------------------------------------- 1 | require 'restclient' 2 | require 'rack' 3 | 4 | module RestClient 5 | module Rack 6 | class Compatibility 7 | def initialize(app) 8 | @app = app 9 | end 10 | 11 | def call(env) 12 | status, header, body = @app.call(env) 13 | net_http_response = RestClient::MockNetHTTPResponse.new(body, status, header) 14 | content = "" 15 | net_http_response.body.each{|line| content << line} 16 | request = env['restclient.hash'][:request] 17 | response = case RestClient::Response.method(:create).arity 18 | when 4 then RestClient::Response.create(content, net_http_response, request, self) 19 | else RestClient::Response.create(content, net_http_response, request) 20 | end 21 | if block = env['restclient.hash'][:block] 22 | block.call(response) 23 | # only raise error if response is not successful 24 | elsif !(200...300).include?(response.code) && e = env['restclient.hash'][:error] 25 | raise e 26 | else 27 | response 28 | end 29 | end 30 | end 31 | end 32 | 33 | class < true, 45 | # :metastore => 'file:/var/cache/rack/meta' 46 | # :entitystore => 'file:/var/cache/rack/body' 47 | # 48 | # Transparent logging of HTTP requests (commonlog format): 49 | # 50 | # RestClient.enable Rack::CommonLogger, STDOUT 51 | # 52 | # Please refer to the documentation of each rack component for the list of 53 | # available options. 54 | # 55 | def self.enable(component, *args) 56 | # remove any existing component of the same class 57 | disable(component) 58 | if component == RestClient::Rack::Compatibility 59 | @components.push [component, args] 60 | else 61 | @components.unshift [component, args] 62 | end 63 | end 64 | 65 | # Disable a component 66 | # 67 | # RestClient.disable Rack::Cache 68 | # => array of remaining components 69 | def self.disable(component) 70 | @components.delete_if{|(existing_component, options)| component == existing_component} 71 | end 72 | 73 | # Returns true if the given component is enabled, false otherwise. 74 | # 75 | # RestClient.enable Rack::Cache 76 | # RestClient.enabled?(Rack::Cache) 77 | # => true 78 | def self.enabled?(component) 79 | !@components.detect{|(existing_component, options)| component == existing_component}.nil? 80 | end 81 | 82 | def self.reset 83 | # hash of the enabled components 84 | @components = [[RestClient::Rack::Compatibility]] 85 | end 86 | 87 | def self.debeautify_headers(headers = {}) # :nodoc: 88 | headers.inject({}) do |out, (key, value)| 89 | out[key.to_s.gsub(/_/, '-').split("-").map{|w| w.capitalize}.join("-")] = value.to_s 90 | out 91 | end 92 | end 93 | 94 | reset 95 | 96 | # Reopen the RestClient::Request class to add a level of indirection in 97 | # order to create the stack of Rack middleware. 98 | class Request 99 | alias_method :original_execute, :execute 100 | def execute(&block) 101 | uri = URI.parse(@url) 102 | # minimal rack spec 103 | env = { 104 | "restclient.hash" => { 105 | :request => self, 106 | :error => nil, 107 | :block => block 108 | }, 109 | "REQUEST_METHOD" => @method.to_s.upcase, 110 | "SCRIPT_NAME" => "", 111 | "PATH_INFO" => uri.path || "/", 112 | "QUERY_STRING" => uri.query || "", 113 | "SERVER_NAME" => uri.host, 114 | "SERVER_PORT" => uri.port.to_s, 115 | "rack.version" => ::Rack::VERSION, 116 | "rack.run_once" => false, 117 | "rack.multithread" => true, 118 | "rack.multiprocess" => true, 119 | "rack.url_scheme" => uri.scheme, 120 | "rack.input" => payload || StringIO.new().set_encoding("ASCII-8BIT"), 121 | "rack.errors" => $stderr 122 | } 123 | @processed_headers.each do |key, value| 124 | env.merge!("HTTP_"+key.to_s.gsub("-", "_").upcase => value) 125 | end 126 | if content_type = env.delete('HTTP_CONTENT_TYPE') 127 | env['CONTENT_TYPE'] = content_type 128 | end 129 | if content_length = env.delete('HTTP_CONTENT_LENGTH') 130 | env['CONTENT_LENGTH'] = content_length 131 | end 132 | stack = RestClient::RACK_APP 133 | RestClient.components.each do |(component, args)| 134 | if (args || []).empty? 135 | stack = component.new(stack) 136 | else 137 | stack = component.new(stack, *args) 138 | end 139 | end 140 | response = stack.call(env) 141 | # allow to use the response block, even if not using the Compatibility component 142 | unless RestClient.enabled?(RestClient::Rack::Compatibility) 143 | if block = env['restclient.hash'][:block] 144 | block.call(response) 145 | end 146 | end 147 | response 148 | end 149 | end 150 | 151 | module Payload 152 | class Base 153 | def rewind(*args) 154 | @stream.rewind(*args) 155 | end 156 | 157 | def gets(*args) 158 | @stream.gets(*args) 159 | end 160 | 161 | def each(&block) 162 | @stream.each(&block) 163 | end 164 | end 165 | end 166 | 167 | # A class that mocks the behaviour of a Net::HTTPResponse class. It is 168 | # required since RestClient::Response must be initialized with a class that 169 | # responds to :code and :to_hash. 170 | class MockNetHTTPResponse 171 | attr_reader :body, :header, :status 172 | alias_method :code, :status 173 | 174 | def initialize(body, status, header) 175 | @body = body 176 | @status = status 177 | @header = header 178 | end 179 | 180 | def to_hash 181 | @header.inject({}) {|out, (key, value)| 182 | # In Net::HTTP, header values are arrays 183 | out[key] = [value] 184 | out 185 | } 186 | end 187 | end 188 | 189 | RACK_APP = Proc.new { |env| 190 | begin 191 | # get the original request, replace headers with those of env, and execute it 192 | request = env['restclient.hash'][:request] 193 | env_headers = (env.keys.select{|k| k=~/^HTTP_/}).inject({}){|accu, k| 194 | accu[k.gsub("HTTP_", "").split("_").map{|s| s.downcase.capitalize}.join("-")] = env[k] 195 | accu 196 | } 197 | env_headers['Content-Type'] = env['CONTENT_TYPE'] if env['CONTENT_TYPE'] 198 | env_headers['Content-Length'] = env['CONTENT_LENGTH'] if env['CONTENT_LENGTH'] 199 | 200 | env['rack.input'].rewind 201 | payload = env['rack.input'].read 202 | payload = (payload.empty? ? nil : Payload.generate(payload)) 203 | request.instance_variable_set "@payload", payload 204 | 205 | headers = request.make_headers(env_headers) 206 | request.processed_headers.update(headers) 207 | response = request.original_execute 208 | rescue RestClient::ExceptionWithResponse => e 209 | # re-raise the error if the response is nil (RestClient does not 210 | # differentiate between a RestClient::RequestTimeout due to a 408 status 211 | # code, and a RestClient::RequestTimeout due to a Timeout::Error) 212 | raise e if e.response.nil? 213 | env['restclient.hash'][:error] = e 214 | response = e.response 215 | end 216 | # to satisfy Rack::Lint 217 | response.headers.delete(:status) 218 | header = RestClient.debeautify_headers( response.headers ) 219 | body = response.to_s 220 | # return the real content-length since RestClient does not do it when 221 | # decoding gzip responses 222 | header['Content-Length'] = body.length.to_s if header.has_key?('Content-Length') 223 | [response.code, header, [body]] 224 | } 225 | 226 | end 227 | -------------------------------------------------------------------------------- /rest-client-components.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "rest-client-components" 3 | s.version = "1.5.0" 4 | 5 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 6 | s.require_paths = ["lib"] 7 | s.authors = ["Cyril Rohr"] 8 | s.date = "2015-04-03" 9 | s.description = "RestClient on steroids ! Easily add one or more Rack middleware around RestClient to add functionalities such as transparent caching (Rack::Cache), transparent logging, etc." 10 | s.email = "cyril.rohr@gmail.com" 11 | s.extra_rdoc_files = [ 12 | "LICENSE", 13 | "README.rdoc" 14 | ] 15 | s.files = [ 16 | "LICENSE", 17 | "README.rdoc", 18 | "Rakefile", 19 | "VERSION", 20 | "examples/beautify_html.rb", 21 | "examples/caching.rb", 22 | "examples/parsing.rb", 23 | "lib/restclient/components.rb", 24 | "rest-client-components.gemspec", 25 | "spec/components_spec.rb", 26 | "spec/spec_helper.rb" 27 | ] 28 | s.homepage = "http://github.com/crohr/rest-client-components" 29 | s.rubygems_version = "2.4.5" 30 | s.summary = "RestClient on steroids ! Easily add one or more Rack middleware around RestClient to add functionalities such as transparent caching (Rack::Cache), transparent logging, etc." 31 | s.license = "MIT" 32 | 33 | if s.respond_to? :specification_version then 34 | s.specification_version = 4 35 | 36 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 37 | s.add_runtime_dependency(%q, [">= 1.6.0"]) 38 | s.add_runtime_dependency(%q, [">= 1.0.1"]) 39 | s.add_development_dependency(%q, [">= 1.21"]) 40 | s.add_development_dependency(%q, [">= 3.2.0"]) 41 | else 42 | s.add_dependency(%q, [">= 1.6.0"]) 43 | s.add_dependency(%q, [">= 1.0.1"]) 44 | s.add_dependency(%q, [">= 1.21"]) 45 | s.add_dependency(%q, [">= 3.2.0"]) 46 | end 47 | else 48 | s.add_dependency(%q, [">= 1.6.0"]) 49 | s.add_dependency(%q, [">= 1.0.1"]) 50 | s.add_dependency(%q, [">= 1.21"]) 51 | s.add_dependency(%q, [">= 3.2.0"]) 52 | end 53 | end 54 | 55 | -------------------------------------------------------------------------------- /spec/components_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | require File.dirname(__FILE__) + '/../lib/restclient/components' 3 | require 'logger' 4 | require 'rack/cache' 5 | require 'time' 6 | describe "Components for RestClient" do 7 | before(:each) do 8 | RestClient.reset 9 | end 10 | 11 | it "should automatically have the Compatibility component enabled" do 12 | RestClient.components.last.should == [RestClient::Rack::Compatibility] 13 | end 14 | it "should enable components" do 15 | RestClient.enable Rack::Cache, :key => "value" 16 | RestClient.enabled?(Rack::Cache).should be_truthy 17 | RestClient.components.first.should == [Rack::Cache, [{:key => "value"}]] 18 | RestClient.enable Rack::CommonLogger 19 | RestClient.components.length.should == 3 20 | RestClient.components.first.should == [Rack::CommonLogger, []] 21 | RestClient.components.last.should == [RestClient::Rack::Compatibility] 22 | end 23 | 24 | it "should allow to disable components" do 25 | RestClient.enable Rack::Cache, :key => "value" 26 | RestClient.disable RestClient::Rack::Compatibility 27 | RestClient.components.length.should == 1 28 | RestClient.components.first.should == [Rack::Cache, [{:key => "value"}]] 29 | end 30 | 31 | it "should always put the RestClient::Rack::Compatibility component last on the stack" do 32 | RestClient.enable Rack::Cache, :key => "value" 33 | RestClient.disable RestClient::Rack::Compatibility 34 | RestClient.enable RestClient::Rack::Compatibility 35 | RestClient.components.last.should == [RestClient::Rack::Compatibility, []] 36 | end 37 | 38 | describe "with Compatibility component" do 39 | before do 40 | RestClient.enable RestClient::Rack::Compatibility 41 | RestClient.enable Rack::Lint 42 | end 43 | it "should work with blocks" do 44 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Length' => 4, 'Content-Type' => 'text/plain'}) 45 | lambda{ RestClient.get "http://server.ltd/resource" do |response| 46 | raise Exception.new(response.code) 47 | end}.should raise_error(Exception, "200") 48 | end 49 | it "should correctly use the response.return! helper" do 50 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 404, :body => "body", :headers => {'Content-Length' => 4, 'Content-Type' => 'text/plain'}) 51 | lambda{ RestClient.get "http://server.ltd/resource" do |response| 52 | response.return! 53 | end}.should raise_error(RestClient::ResourceNotFound) 54 | end 55 | it "should raise ExceptionWithResponse errors" do 56 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 404, :body => "body", :headers => {'Content-Length' => 4, 'Content-Type' => 'text/plain'}) 57 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ResourceNotFound) 58 | end 59 | it "should raise Exception errors" do 60 | stub_request(:get, "http://server.ltd/resource").to_raise(EOFError) 61 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ServerBrokeConnection) 62 | end 63 | it "should raise timeout Exception errors" do 64 | stub_request(:get, "http://server.ltd/resource").to_raise(Timeout::Error) 65 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::RequestTimeout) 66 | end 67 | it "should correctly pass the payload in rack.input" do 68 | class RackAppThatProcessesPayload 69 | def initialize(app); @app = app; end 70 | def call(env) 71 | env['rack.input'].rewind 72 | env['rack.input'] = StringIO.new(env['rack.input'].read.gsub(/rest-client/, "rest-client-components")) 73 | env['CONTENT_TYPE'] = "text/html" 74 | @app.call(env) 75 | end 76 | end 77 | RestClient.enable RackAppThatProcessesPayload 78 | stub_request(:post, "http://server.ltd/resource").with(:body => "rest-client-components is cool", :headers => {'Content-Type'=>'text/html', 'Accept-Encoding'=>'gzip, deflate', 'Content-Length'=>'37', 'Accept'=>'*/*'}).to_return(:status => 201, :body => "ok", :headers => {'Content-Length' => 2, 'Content-Type' => "text/plain"}) 79 | RestClient.post "http://server.ltd/resource", 'rest-client is cool', :content_type => "text/plain" 80 | end 81 | 82 | it "should correctly pass content-length and content-type headers" do 83 | stub_request(:post, "http://server.ltd/resource").with(:body => "some stupid message", :headers => {'Content-Type'=>'text/plain', 'Accept-Encoding'=>'gzip, deflate', 'Content-Length'=>'19', 'Accept'=>'*/*'}).to_return(:status => 201, :body => "ok", :headers => {'Content-Length' => 2, 'Content-Type' => "text/plain"}) 84 | RestClient.post "http://server.ltd/resource", 'some stupid message', :content_type => "text/plain", :content_length => 19 85 | end 86 | 87 | describe "and another component" do 88 | before do 89 | class AnotherRackMiddleware 90 | def initialize(app); @app=app; end 91 | def call(env) 92 | env['HTTP_X_SPECIFIC_HEADER'] = 'value' 93 | @app.call(env) 94 | end 95 | end 96 | RestClient.enable AnotherRackMiddleware 97 | end 98 | it "should correctly pass the headers set by other components" do 99 | stub_request(:get, "http://server.ltd/resource").with(:headers => {'X-Specific-Header' => 'value'}).to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 100 | RestClient.get "http://server.ltd/resource" 101 | end 102 | end 103 | 104 | describe "with Rack::Cache enabled" do 105 | before(:each) do 106 | random = SecureRandom.uuid 107 | RestClient.enable Rack::Cache, 108 | :metastore => "heap:/#{random}/", 109 | :entitystore => "heap:/#{random}/" 110 | end 111 | it "should raise ExceptionWithResponse errors" do 112 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 404, :body => "body", :headers => {'Content-Length' => 4, 'Content-Type' => 'text/plain'}) 113 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ResourceNotFound) 114 | end 115 | it "should raise Exception errors" do 116 | stub_request(:get, "http://server.ltd/resource").to_raise(EOFError) 117 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ServerBrokeConnection) 118 | end 119 | it "should return a RestClient::Response" do 120 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 121 | RestClient.get "http://server.ltd/resource" do |response| 122 | response.code.should == 200 123 | response.headers[:x_rack_cache].should == 'miss' 124 | response.body.should == "body" 125 | end 126 | end 127 | it "should get cached" do 128 | now = Time.now 129 | last_modified = Time.at(now-3600) 130 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Cache-Control' => 'public', 'Content-Length' => 4, 'Date' => now.httpdate, 'Last-Modified' => last_modified.httpdate}).times(1).then. 131 | to_return(:status => 304, :headers => {'Content-Type' => 'text/plain', 'Cache-Control' => 'public', 'Content-Length' => 0, 'Date' => now.httpdate, 'Last-Modified' => last_modified.httpdate}) 132 | RestClient.get "http://server.ltd/resource" do |response| 133 | response.headers[:x_rack_cache].should == 'miss, store' 134 | expect(response.headers[:age]).to_not be_nil 135 | expect(response.headers[:age].to_i > 0) 136 | response.body.should == "body" 137 | end 138 | RestClient.get "http://server.ltd/resource" do |response| 139 | response.headers[:x_rack_cache].should == 'stale, valid, store' 140 | response.body.should == "body" 141 | end 142 | end 143 | end 144 | end 145 | 146 | describe "without Compatibility component" do 147 | before do 148 | RestClient.disable RestClient::Rack::Compatibility 149 | RestClient.enable Rack::Lint 150 | end 151 | it "should return response as an array of status, headers, body" do 152 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 153 | lambda{RestClient.get "http://server.ltd/resource" do |response| 154 | raise Exception.new(response.class) 155 | end}.should raise_error(Exception, "Array") 156 | end 157 | it "should return response as an array of status, headers, body if response block is used" do 158 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 159 | status, headers, body = RestClient.get "http://server.ltd/resource" 160 | status.should == 200 161 | headers.should == {"Content-Type"=>"text/plain", "Content-Length"=>"4"} 162 | content = "" 163 | body.each{|block| content << block} 164 | content.should == "body" 165 | end 166 | it "should not raise ExceptionWithResponse exceptions" do 167 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 404, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 168 | status, headers, body = RestClient.get "http://server.ltd/resource" 169 | status.should == 404 170 | headers.should == {"Content-Type"=>"text/plain", "Content-Length"=>"4"} 171 | content = "" 172 | body.each{|block| content << block} 173 | content.should == "body" 174 | end 175 | it "should still raise Exception errors" do 176 | stub_request(:get, "http://server.ltd/resource").to_raise(EOFError) 177 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ServerBrokeConnection) 178 | end 179 | 180 | describe "with Rack::Cache" do 181 | before do 182 | random = SecureRandom.uuid 183 | RestClient.enable Rack::Cache, 184 | :metastore => "heap:/#{random}/", 185 | :entitystore => "heap:/#{random}/" 186 | end 187 | it "should not raise ExceptionWithResponse errors" do 188 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 404, :body => "body", :headers => {'Content-Length' => 4, 'Content-Type' => 'text/plain'}) 189 | status, headers, body = RestClient.get "http://server.ltd/resource" 190 | status.should == 404 191 | headers['X-Rack-Cache'].should == 'miss' 192 | content = "" 193 | body.each{|block| content << block} 194 | content.should == "body" 195 | end 196 | it "should raise Exception errors" do 197 | stub_request(:get, "http://server.ltd/resource").to_raise(EOFError) 198 | lambda{ RestClient.get "http://server.ltd/resource" }.should raise_error(RestClient::ServerBrokeConnection) 199 | end 200 | it "should return an array" do 201 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Content-Length' => 4}) 202 | RestClient.get "http://server.ltd/resource" do |response| 203 | status, headers, body = response 204 | status.should == 200 205 | headers['X-Rack-Cache'].should == 'miss' 206 | content = "" 207 | body.each{|block| content << block} 208 | content.should == "body" 209 | end 210 | end 211 | it "should get cached" do 212 | now = Time.now 213 | last_modified = Time.at(now-3600) 214 | stub_request(:get, "http://server.ltd/resource").to_return(:status => 200, :body => "body", :headers => {'Content-Type' => 'text/plain', 'Cache-Control' => 'public', 'Content-Length' => 4, 'Date' => now.httpdate, 'Last-Modified' => last_modified.httpdate}).times(1).then. 215 | to_return(:status => 304, :headers => {'Content-Type' => 'text/plain', 'Cache-Control' => 'public', 'Content-Length' => 0, 'Date' => now.httpdate, 'Last-Modified' => last_modified.httpdate}) 216 | RestClient.get "http://server.ltd/resource" do |status, headers, body| 217 | headers['X-Rack-Cache'] == 'miss, store' 218 | headers['Age'].should == "0" 219 | end 220 | sleep 1 221 | RestClient.get "http://server.ltd/resource" do |status, headers, body| 222 | headers['X-Rack-Cache'].should == 'stale, valid, store' 223 | headers['Age'].should == "1" 224 | end 225 | end 226 | end 227 | end 228 | 229 | end 230 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'webmock/rspec' 4 | 5 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 6 | 7 | RSpec.configure do |config| 8 | 9 | end 10 | --------------------------------------------------------------------------------