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