transport 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +109 -0
- data/Rakefile +50 -0
- data/lib/transport.rb +9 -0
- data/lib/transport/common.rb +41 -0
- data/lib/transport/common/request_builder.rb +43 -0
- data/lib/transport/http.rb +43 -0
- data/lib/transport/http/formatter.rb +85 -0
- data/lib/transport/http/request_builder.rb +73 -0
- data/lib/transport/http/request_builder/parameter_serializer.rb +61 -0
- data/lib/transport/json.rb +37 -0
- data/lib/transport/json/request_builder.rb +56 -0
- data/lib/transport/json/response_parser.rb +33 -0
- data/lib/transport/spec.rb +10 -0
- data/lib/transport/spec/faker.rb +75 -0
- data/lib/transport/unexpected_status_code_error.rb +21 -0
- data/spec/acceptance/http_spec.rb +20 -0
- data/spec/acceptance/json_spec.rb +10 -0
- data/spec/lib/transport/common/request_builder_spec.rb +61 -0
- data/spec/lib/transport/common_spec.rb +68 -0
- data/spec/lib/transport/http/formatter_spec.rb +28 -0
- data/spec/lib/transport/http/request_builder/parameter_serializer_spec.rb +18 -0
- data/spec/lib/transport/http/request_builder_spec.rb +134 -0
- data/spec/lib/transport/http_spec.rb +61 -0
- data/spec/lib/transport/json/request_builder_spec.rb +78 -0
- data/spec/lib/transport/json/response_parser_spec.rb +52 -0
- data/spec/lib/transport/json_spec.rb +58 -0
- data/spec/lib/transport/spec/faker_spec.rb +87 -0
- data/spec/lib/transport/unexpected_status_code_error_spec.rb +18 -0
- data/spec/spec_helper.rb +6 -0
- metadata +133 -0
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
|