transport 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Philipp Brüll
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.
data/README.rdoc ADDED
@@ -0,0 +1,109 @@
1
+
2
+ = Transport
3
+
4
+ A HTTP/JSON transport layer. Provides a single command interface to perform http/json requests. A spec helper to
5
+ perform request against a fake server is also included.
6
+
7
+ == HTTP requests
8
+
9
+ To perform a http request the call of a single command is needed.
10
+
11
+ Transport::HTTP.request :get, "http://www.google.com", :expected_status_code => 200
12
+
13
+ The method will return a string with the content of Google home page. If another status code is received than the
14
+ expected one, an <tt>Transport::UnexpectedStatusCodeError</tt> will be raised. In that case, the status code can be
15
+ read from the raised error object.
16
+
17
+ begin
18
+ Transport::HTTP.request :get, "http://www.google.com", :expected_status_code => 200
19
+ rescue Transport::UnexpectedStatusCodeError => error
20
+ puts "error code = #{error.status_code}"
21
+ end
22
+
23
+ === Parameters, headers and the request body
24
+
25
+ Parameters and headers can also be passed to the request method.
26
+
27
+ Transport::HTTP.request :get, "http://www.somesite.com",
28
+ :parameters => { "test" => "test value" },
29
+ :headers => { "Accept" => "text/html", "Content-Type" => "application/x-www-form-urlencoded" }
30
+
31
+ Before performing a GET or DELETE request, the given parameters will be encoded in the request url (e.g.
32
+ <tt>?test=test%20value</tt>). POST and PUT request will transport the encoded parameters in their request body, unless
33
+ a request body is explicitly specified.
34
+
35
+ Transport::HTTP.request :post, "http://www.somesite.com",
36
+ :headers => { "Accept" => "text/html", "Content-Type" => "text/html" },
37
+ :body => "<html>some data</html>"
38
+
39
+ === Authentication
40
+
41
+ Currently, BASIC authentication is supported for requests. To enable it, simply pass the parameter <tt>auth_type</tt>
42
+ along with <tt>username</tt> and <tt>password</tt> to the request method.
43
+
44
+ Transport::HTTP.request :get, "http://www.somesite.com",
45
+ :auth_type => :basic, :username => "user", :password => "pass"
46
+
47
+ == JSON requests
48
+
49
+ A JSON request is basically an extensions of a HTTP request. All options that are possible on HTTP request are also
50
+ possible on JSON requests. But a few differences should be mentioned here.
51
+
52
+ * The <tt>Accept</tt> header will be set to <tt>application/json</tt> anyway.
53
+ * If a request body is specified, the <tt>Content-Type</tt> will also be set to <tt>application/json</tt> cause only
54
+ json data should be transferred.
55
+ * If the option <tt>encode_parameters</tt> is set the true, all parameters will be encoded to json, before encoded to
56
+ the url. This makes it possible to transfer more complex parameters like arrays and hashes.
57
+ * The response body will also be parsed to json.
58
+
59
+ === Example
60
+
61
+ Transport::JSON.request :get, "http://www.somesite.com/content.json",
62
+ :auth_type => :basic, :username => "user", :password => "pass",
63
+ :parameters => { "filter" => { "date" => Date.today, "tags" => [ "gossip", "serious" ] } },
64
+ :encode_parameters => true
65
+
66
+ This will perform an authenticated GET request to the given url and passes the json-encoded parameters.
67
+
68
+ == Fake requests
69
+
70
+ For test proposes, this gem also comes with the possibility of doing fake requests. If you want to test your code
71
+ against a webservice, but don't want to dependent on a network connection or on the webservice itself - or just want
72
+ to speed up your tests, you can fake the webservice by calling <tt>Transport::Spec::Faker.fake!</tt>.
73
+
74
+ describe "webservice" do
75
+
76
+ before :each do
77
+ Transport::Spec::Faker.fake! "some_filename.yml"
78
+ end
79
+
80
+ ...
81
+
82
+ end
83
+
84
+ The given filename should point to a YML file that contains an array of all fake requests. The file should look like
85
+ the following example.
86
+
87
+ -- # use a single dash here
88
+ :http_method: "get"
89
+ :url: "http://www.somesite.com/content.json"
90
+ :response:
91
+ :code: 200
92
+ :body:
93
+ "test": "test value"
94
+ -- # use a single dash here
95
+ :http_method: "post"
96
+ :url: "http://www.somesite.com"
97
+ :headers:
98
+ "Accept": "text/html"
99
+ "Content-Type": "text/html"
100
+ :response:
101
+ :code: 200
102
+ :body: "<html>test value</html>"
103
+
104
+ Once the faker is activated, it will try to fake any request. If your software is trying to do a request that isn't
105
+ defined in the YML file, it will raises a <tt>Transport::Spec::Faker::NoFakeRequestError</tt>.
106
+
107
+ == Development
108
+
109
+ This project is still experimental and under development. Any bug report and contribution is welcome!
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+
3
+ gem 'rspec'
4
+ require 'rspec'
5
+ require 'rspec/core/rake_task'
6
+
7
+ gem 'reek'
8
+ require 'reek/rake/task'
9
+
10
+ require 'rake/rdoctask'
11
+
12
+ task :default => :spec
13
+
14
+ namespace :gem do
15
+
16
+ desc "Builds the gem"
17
+ task :build do
18
+ system "gem build *.gemspec && mkdir -p pkg/ && mv *.gem pkg/"
19
+ end
20
+
21
+ desc "Builds and installs the gem"
22
+ task :install => :build do
23
+ system "gem install pkg/"
24
+ end
25
+
26
+ end
27
+
28
+ Reek::Rake::Task.new do |task|
29
+ task.fail_on_error = true
30
+ end
31
+
32
+ desc "Generate the rdoc"
33
+ Rake::RDocTask.new do |rdoc|
34
+ rdoc.rdoc_files.add [ "README.rdoc", "lib/**/*.rb" ]
35
+ rdoc.main = "README.rdoc"
36
+ end
37
+
38
+ desc "Run all specs in spec directory"
39
+ RSpec::Core::RakeTask.new do |task|
40
+ task.pattern = "spec/lib/**/*_spec.rb"
41
+ end
42
+
43
+ namespace :spec do
44
+
45
+ desc "Run all integration specs in spec/acceptance directory"
46
+ RSpec::Core::RakeTask.new(:acceptance) do |task|
47
+ task.pattern = "spec/acceptance/**/*_spec.rb"
48
+ end
49
+
50
+ end
data/lib/transport.rb ADDED
@@ -0,0 +1,9 @@
1
+
2
+ module Transport
3
+
4
+ autoload :Common, File.join(File.dirname(__FILE__), "transport", "common")
5
+ autoload :HTTP, File.join(File.dirname(__FILE__), "transport", "http")
6
+ autoload :JSON, File.join(File.dirname(__FILE__), "transport", "json")
7
+ autoload :UnexpectedStatusCodeError, File.join(File.dirname(__FILE__), "transport", "unexpected_status_code_error")
8
+
9
+ end
@@ -0,0 +1,41 @@
1
+
2
+ module Transport
3
+
4
+ module Common
5
+
6
+ autoload :RequestBuilder, File.join(File.dirname(__FILE__), "common", "request_builder")
7
+
8
+ def self.included(base_class)
9
+ base_class.class_eval do
10
+ include InstanceMethods
11
+ extend ClassMethods
12
+ end
13
+ end
14
+
15
+ module InstanceMethods
16
+
17
+ attr_reader :uri
18
+ attr_reader :request
19
+ attr_reader :options
20
+ attr_reader :response
21
+
22
+ def initialize(uri, request, options = { })
23
+ @uri, @request, @options = uri, request, options
24
+ end
25
+
26
+ end
27
+
28
+ module ClassMethods
29
+
30
+ def request(http_method, url, options = { })
31
+ uri, request = self::RequestBuilder.build http_method, url, options
32
+ transport = new uri, request, options
33
+ transport.perform
34
+ transport.response
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,43 @@
1
+ require 'uri'
2
+
3
+ module Transport
4
+
5
+ module Common
6
+
7
+ module RequestBuilder
8
+
9
+ def self.included(base_class)
10
+ base_class.class_eval do
11
+ include InstanceMethods
12
+
13
+ def self.build(*arguments)
14
+ request_builder = new *arguments
15
+ request_builder.perform
16
+ [ request_builder.uri, request_builder.request ]
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+
24
+ HTTP_METHODS_WITH_PARAMETERS = [ :get, :delete ].freeze unless defined?(HTTP_METHODS_WITH_PARAMETERS)
25
+ HTTP_METHODS_WITH_BODY = [ :post, :put ].freeze unless defined?(HTTP_METHODS_WITH_BODY)
26
+
27
+ attr_reader :http_method
28
+ attr_reader :url
29
+ attr_reader :options
30
+ attr_reader :uri
31
+
32
+ def initialize(http_method, url, options = { })
33
+ @http_method, @url, @options = http_method, url, options
34
+ @uri = URI.parse @url
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,43 @@
1
+ require 'net/http'
2
+ require File.join(File.dirname(__FILE__), "common")
3
+
4
+ module Transport
5
+
6
+ # Common transport layer for http transfers.
7
+ class HTTP
8
+ include Common
9
+
10
+ autoload :RequestBuilder, File.join(File.dirname(__FILE__), "http", "request_builder")
11
+ autoload :Formatter, File.join(File.dirname(__FILE__), "http", "formatter")
12
+
13
+ def perform
14
+ perform_request
15
+ log_transport
16
+ check_status_code
17
+ end
18
+
19
+ private
20
+
21
+ def perform_request
22
+ @http_response = Net::HTTP.start(@uri.host, @uri.port) do |connection|
23
+ connection.request @request
24
+ end
25
+ @response = @http_response.body
26
+ end
27
+
28
+ def log_transport
29
+ @formatter ||= Formatter.new @options[:logger]
30
+ @formatter.log_transport @uri, @request, @http_response
31
+ end
32
+
33
+ def check_status_code
34
+ expected_status_code = @options[:expected_status_code]
35
+ return unless expected_status_code
36
+ response_code = @http_response.code.to_i
37
+ response_body = @http_response.body
38
+ raise UnexpectedStatusCodeError.new(response_code, response_body) if expected_status_code.to_i != response_code
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,85 @@
1
+
2
+ module Transport
3
+
4
+ # Common transport layer for http transfers.
5
+ class HTTP
6
+
7
+ # Formatter for the http request and response objects. Used for log output.
8
+ class Formatter
9
+
10
+ # Request formatter.
11
+ class Request
12
+
13
+ def initialize(uri, request)
14
+ @uri, @request = uri, request
15
+ end
16
+
17
+ def message
18
+ "transport to #{@uri.host} #{@uri.port}\n" +
19
+ request
20
+ end
21
+
22
+ private
23
+
24
+ def request
25
+ "request: #{@request.class} #{@request.path}\n" +
26
+ request_headers +
27
+ request_body
28
+ end
29
+
30
+ def request_headers
31
+ message = ""
32
+ @request.each_capitalized_name do |key|
33
+ message += " #{key}: #{@request[key]}\n"
34
+ end
35
+ message == "" ? "" : " headers:\n" + message
36
+ end
37
+
38
+ def request_body
39
+ request_body = @request.body
40
+ request_body ? " body:\n" + Formatter.intend(" #{request_body}", 4) : ""
41
+ end
42
+
43
+ end
44
+
45
+ # Response formatter.
46
+ class Response
47
+
48
+ def initialize(response)
49
+ @response = response
50
+ end
51
+
52
+ def message
53
+ message = "response: #{@response.code}\n"
54
+ message += Formatter.intend(" #{@response.body}")
55
+ message
56
+ end
57
+
58
+ end
59
+
60
+ attr_reader :logger
61
+
62
+ def initialize(logger)
63
+ @logger = logger
64
+ end
65
+
66
+ def log_transport(uri, request, response)
67
+ log Request.new(uri, request).message + "\n" + Response.new(response).message
68
+ end
69
+
70
+ private
71
+
72
+ def log(message)
73
+ return unless @logger
74
+ @logger.info message
75
+ end
76
+
77
+ def self.intend(message, spaces = 2)
78
+ message.gsub "\n", "\n" + (" " * spaces)
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,73 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+
4
+ module Transport
5
+
6
+ # Common transport layer for http transfers.
7
+ class HTTP
8
+
9
+ # Builder for the http transport layer requests.
10
+ class RequestBuilder
11
+ include Common::RequestBuilder
12
+
13
+ autoload :ParameterSerializer, File.join(File.dirname(__FILE__), "request_builder", "parameter_serializer")
14
+
15
+ attr_reader :request
16
+
17
+ def perform
18
+ initialize_request_class
19
+ initialize_request_path
20
+ initialize_request
21
+ initialize_request_authentication
22
+ initialize_request_body
23
+ end
24
+
25
+ private
26
+
27
+ def initialize_request_class
28
+ request_class_name = @http_method.to_s.capitalize
29
+ raise NotImplementedError, "the request method #{@http_method} is not implemented" unless Net::HTTP.const_defined?(request_class_name)
30
+ @request_class = Net::HTTP.const_get request_class_name
31
+ end
32
+
33
+ def initialize_request_path
34
+ uri = URI.parse @url
35
+ generate_query
36
+ @request_path = uri.path
37
+ @request_path += "/" if @request_path == ""
38
+ @request_path += "?" + @query if HTTP_METHODS_WITH_PARAMETERS.include?(@http_method.to_sym) && @query
39
+ end
40
+
41
+ def initialize_request
42
+ @request = @request_class.new @request_path, @options[:headers]
43
+ end
44
+
45
+ def initialize_request_authentication
46
+ auth_type = @options[:auth_type]
47
+ return unless auth_type
48
+ send :"initialize_request_#{auth_type}_authentication"
49
+ rescue NoMethodError
50
+ raise NotImplementedError, "the given auth_type [#{auth_type}] is not implemented"
51
+ end
52
+
53
+ def initialize_request_basic_authentication
54
+ @request.basic_auth @options[:username], @options[:password]
55
+ end
56
+
57
+ def initialize_request_body
58
+ @request.body = (@options.has_key?(:body) ? @options[:body] : generate_query) if HTTP_METHODS_WITH_BODY.include?(@http_method.to_sym)
59
+ end
60
+
61
+ def generate_query
62
+ @query ||= begin
63
+ serializer = ParameterSerializer.new @options[:parameters]
64
+ serializer.perform
65
+ serializer.result
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end