transport 1.0.0

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.
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