aitch 0.1.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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --order random --format documentation
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ script: "bundle exec rspec"
3
+
4
+ rvm:
5
+ - 2.0.0
6
+ - 1.9.3
7
+
8
+ gemfile:
9
+ - Gemfile
10
+
11
+ notifications:
12
+ email:
13
+ - fnando.vieira@gmail.com
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Nando Vieira
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,141 @@
1
+ # Aitch
2
+
3
+ [![Build Status](https://travis-ci.org/fnando/aitch.png)](https://travis-ci.org/fnando/aitch)
4
+
5
+ [![CodeClimate](https://codeclimate.com/github/fnando/aitch.png)](https://codeclimate.com/github/fnando/aitch/)
6
+
7
+ [![RubyGems](https://badge.fury.io/rb/aitch.png)](https://rubygems.org/gems/aitch)
8
+
9
+ A simple HTTP client.
10
+
11
+ Features:
12
+
13
+ * Supports Gzip|Deflate response
14
+ * Automatically parses JSON and XML responses
15
+ * Automatically follows redirect
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'aitch'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install aitch
32
+
33
+ ## Usage
34
+
35
+ ### Configuration
36
+
37
+ These are the default settings:
38
+
39
+ ```ruby
40
+ Aitch.configure do |config|
41
+ # Set request timeout.
42
+ config.timeout = 5
43
+
44
+ # Set default headers.
45
+ config.default_headers = {}
46
+
47
+ # Set follow redirect.
48
+ config.follow_redirect = true
49
+
50
+ # Set redirection limit.
51
+ config.redirect_limit = 5
52
+
53
+ # Set the user agent.
54
+ config.user_agent = "Aitch/0.0.1 (http://rubygems.org/gems/aitch)"
55
+
56
+ # Set the logger.
57
+ config.logger = nil
58
+
59
+ # Set the JSON parser.
60
+ config.json_parser = JSON
61
+
62
+ # Set the XML parser.
63
+ config.xml_parser = Aitch::XMLParser
64
+ end
65
+ ```
66
+
67
+ ### Requests
68
+
69
+ Performing requests:
70
+
71
+ ```ruby
72
+ response = Aitch.get("http://example.org", params, headers)
73
+ Aitch.post("http://example.org", params, headers)
74
+ Aitch.put("http://example.org", params, headers)
75
+ Aitch.patch("http://example.org", params, headers)
76
+ Aitch.delete("http://example.org", params, headers)
77
+ Aitch.options("http://example.org", params, headers)
78
+ Aitch.trace("http://example.org", params, headers)
79
+ Aitch.head("http://example.org", params, headers)
80
+ ```
81
+
82
+ ### Response
83
+
84
+ The response object:
85
+
86
+ ```ruby
87
+ response.html?
88
+ response.xml?
89
+ response.json?
90
+ response.content_type
91
+ response.headers
92
+ response.location
93
+ response.success? # status >= 200 && status <= 399
94
+ response.redirect? # status 3xx
95
+ response.error? # status 4xx or 5xx
96
+ response.error # response error
97
+ response.body # returned body
98
+ response.data # JSON or XML payload
99
+ ```
100
+
101
+ ### Following redirects
102
+
103
+ The configuration:
104
+
105
+ ```ruby
106
+ Aitch.configure do |config|
107
+ config.follow_redirect = true
108
+ config.redirect_limit = 10
109
+ end
110
+ ```
111
+
112
+ The request:
113
+
114
+ ```ruby
115
+ Aitch.get("http://example.org")
116
+ ```
117
+
118
+ If the redirect limit is exceeded, then the `Aitch::TooManyRedirectsError` exception
119
+ is raised.
120
+
121
+ ### Basic auth
122
+
123
+ Setting basic auth credentials:
124
+
125
+ ```ruby
126
+ Aitch.get("http://restrict.example.org/", {}, {}, user: "john", password: "test")
127
+ ```
128
+
129
+ ### Setting custom headers
130
+
131
+ ```ruby
132
+ Aitch.get("http://example.org", {}, {"User-Agent" => "MyBot/1.0.0"})
133
+ ```
134
+
135
+ ## Contributing
136
+
137
+ 1. Fork it
138
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
139
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
140
+ 4. Push to the branch (`git push origin my-new-feature`)
141
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'aitch/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "aitch"
8
+ spec.version = Aitch::VERSION
9
+ spec.authors = ["Nando Vieira"]
10
+ spec.email = ["fnando.vieira@gmail.com"]
11
+ spec.description = "A simple HTTP client"
12
+ spec.summary = spec.description
13
+ spec.homepage = "http://rubygems.org/gems/aitch"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "pry-meta"
25
+ spec.add_development_dependency "test_notifier"
26
+ spec.add_development_dependency "fakeweb"
27
+ spec.add_development_dependency "nokogiri"
28
+ end
@@ -0,0 +1,47 @@
1
+ require "net/https"
2
+ require "forwardable"
3
+ require "json"
4
+ require "zlib"
5
+
6
+ require "aitch/configuration"
7
+ require "aitch/errors"
8
+ require "aitch/request"
9
+ require "aitch/redirect"
10
+ require "aitch/response"
11
+ require "aitch/response/errors"
12
+ require "aitch/response/body"
13
+ require "aitch/xml_parser"
14
+ require "aitch/version"
15
+
16
+ module Aitch
17
+ extend self
18
+
19
+ def execute(method, url, args = {}, headers = {}, options = {})
20
+ Request.new(method, url, args, headers, options).perform
21
+ end
22
+
23
+ def execute!(*args)
24
+ response = execute(*args)
25
+ raise response.error if response.error?
26
+ response
27
+ end
28
+
29
+ %w[
30
+ get
31
+ post
32
+ put
33
+ patch
34
+ delete
35
+ options
36
+ trace
37
+ head
38
+ ].each do |method_name|
39
+ define_method(method_name) do |url, args = {}, headers = {}, options = {}|
40
+ execute(method_name, url, args, headers, options)
41
+ end
42
+
43
+ define_method("#{method_name}!") do |url, args = {}, headers = {}, options = {}|
44
+ execute!(method_name, url, args, headers, options)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ module Aitch
2
+ class Configuration
3
+ # Set proxy.
4
+ attr_accessor :proxy
5
+
6
+ # Set request timeout.
7
+ attr_accessor :timeout
8
+
9
+ # Set default headers.
10
+ attr_accessor :default_headers
11
+
12
+ # Set follow redirect.
13
+ attr_accessor :follow_redirect
14
+
15
+ # Set redirection limit.
16
+ attr_accessor :redirect_limit
17
+
18
+ # Set the user agent.
19
+ attr_accessor :user_agent
20
+
21
+ # Set the logger.
22
+ attr_accessor :logger
23
+
24
+ # Set the JSON parser.
25
+ attr_accessor :json_parser
26
+
27
+ # Set the XML parser.
28
+ attr_accessor :xml_parser
29
+
30
+ def initialize
31
+ @timeout = 10
32
+ @redirect_limit = 5
33
+ @follow_redirect = true
34
+ @user_agent = "Aitch/#{Aitch::VERSION} (http://rubygems.org/gems/aitch)"
35
+ @default_headers = {}
36
+ @json_parser = JSON
37
+ @xml_parser = XMLParser
38
+ end
39
+ end
40
+
41
+ def self.configure(&block)
42
+ yield configuration
43
+ end
44
+
45
+ def self.configuration
46
+ @configuration ||= Configuration.new
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ module Aitch
2
+ InvalidURIError = Class.new(StandardError)
3
+ InvalidHTTPMethodError = Class.new(StandardError)
4
+ RequestTimeoutError = Class.new(StandardError)
5
+ TooManyRedirectsError = Class.new(StandardError)
6
+
7
+ ResponseError = Class.new(StandardError)
8
+ BadRequestError = Class.new(ResponseError)
9
+ UnauthorizedError = Class.new(ResponseError)
10
+ PaymentRequiredError = Class.new(ResponseError)
11
+ ForbiddenError = Class.new(ResponseError)
12
+ NotFoundError = Class.new(ResponseError)
13
+ MethodNotAllowedError = Class.new(ResponseError)
14
+ NotAcceptableError = Class.new(ResponseError)
15
+ ProxyAuthenticationRequiredError = Class.new(ResponseError)
16
+ RequestTimeOutError = Class.new(ResponseError)
17
+ ConflictError = Class.new(ResponseError)
18
+ GoneError = Class.new(ResponseError)
19
+ LengthRequiredError = Class.new(ResponseError)
20
+ PreconditionFailedError = Class.new(ResponseError)
21
+ RequestEntityTooLargeError = Class.new(ResponseError)
22
+ RequestURITooLongError = Class.new(ResponseError)
23
+ UnsupportedMediaTypeError = Class.new(ResponseError)
24
+ RequestedRangeNotSatisfiableError = Class.new(ResponseError)
25
+ ExpectationFailedError = Class.new(ResponseError)
26
+ UnprocessableEntityError = Class.new(ResponseError)
27
+ LockedError = Class.new(ResponseError)
28
+ FailedDependencyError = Class.new(ResponseError)
29
+ UpgradeRequiredError = Class.new(ResponseError)
30
+ PreconditionRequiredError = Class.new(ResponseError)
31
+ TooManyRequestsError = Class.new(ResponseError)
32
+ RequestHeaderFieldsTooLargeError = Class.new(ResponseError)
33
+ InternalServerErrorError = Class.new(ResponseError)
34
+ NotImplementedError = Class.new(ResponseError)
35
+ BadGatewayError = Class.new(ResponseError)
36
+ ServiceUnavailableError = Class.new(ResponseError)
37
+ GatewayTimeOutError = Class.new(ResponseError)
38
+ VersionNotSupportedError = Class.new(ResponseError)
39
+ InsufficientStorageError = Class.new(ResponseError)
40
+ NetworkAuthenticationRequiredError = Class.new(ResponseError)
41
+ end
@@ -0,0 +1,20 @@
1
+ module Aitch
2
+ class Redirect
3
+ def initialize
4
+ @tries = 1
5
+ @max_tries = Aitch.configuration.redirect_limit
6
+ end
7
+
8
+ def followed!
9
+ @tries += 1
10
+ end
11
+
12
+ def follow?(response)
13
+ enabled? && response.redirect? && @tries < @max_tries
14
+ end
15
+
16
+ def enabled?
17
+ Aitch.configuration.follow_redirect
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,105 @@
1
+ module Aitch
2
+ class Request
3
+ def initialize(request_method, url, args = {}, headers = {}, options = {})
4
+ @request_method = request_method
5
+ @url = url
6
+ @args = args
7
+ @headers = headers
8
+ @options = options
9
+ end
10
+
11
+ def perform
12
+ response = Response.new(client.request(request))
13
+ redirect = Redirect.new
14
+
15
+ while redirect.follow?(response)
16
+ redirect.followed!
17
+ response = Aitch.get(response.location)
18
+ end
19
+
20
+ raise TooManyRedirectsError if redirect.enabled? && response.redirect?
21
+
22
+ response
23
+ rescue timeout_exception
24
+ raise RequestTimeoutError
25
+ end
26
+
27
+ def request
28
+ @request ||= http_method_class.new(File.join("/", uri.path)).tap do |request|
29
+ set_body(request)
30
+ set_user_agent(request)
31
+ set_gzip(request)
32
+ set_headers(request)
33
+ set_credentials(request)
34
+ end
35
+ end
36
+
37
+ def client
38
+ @client ||= Net::HTTP.new(uri.host, uri.port).tap do |client|
39
+ set_https(client)
40
+ set_timeout(client)
41
+ set_logger(client)
42
+ end
43
+ end
44
+
45
+ def uri
46
+ @uri ||= URI.parse(@url)
47
+ rescue URI::InvalidURIError
48
+ raise InvalidURIError
49
+ end
50
+
51
+ def http_method_class
52
+ Net::HTTP.const_get(@request_method.to_s.capitalize)
53
+ rescue NameError
54
+ raise InvalidHTTPMethodError, "unexpected HTTP verb: #{@request_method.inspect}"
55
+ end
56
+
57
+ private
58
+ def set_body(request)
59
+ if @args.respond_to?(:to_h)
60
+ request.form_data = @args.to_h
61
+ elsif @args.kind_of?(Hash)
62
+ request.form_data = @args
63
+ else
64
+ request.body = @args.to_s
65
+ end
66
+ end
67
+
68
+ def set_headers(request)
69
+ all_headers = Aitch.configuration.default_headers.merge(@headers)
70
+ all_headers.each do |name, value|
71
+ request[name.to_s] = value.to_s
72
+ end
73
+ end
74
+
75
+ def set_credentials(request)
76
+ request.basic_auth(@options[:user], @options[:password]) if @options[:user]
77
+ end
78
+
79
+ def set_https(client)
80
+ client.use_ssl = uri.scheme == "https"
81
+ client.verify_mode = OpenSSL::SSL::VERIFY_PEER
82
+ end
83
+
84
+ def set_timeout(client)
85
+ client.read_timeout = Aitch.configuration.timeout
86
+ end
87
+
88
+ def set_logger(client)
89
+ logger = Aitch.configuration.logger
90
+ client.set_debug_output(logger) if logger
91
+ end
92
+
93
+ def set_user_agent(request)
94
+ request["User-Agent"] = Aitch.configuration.user_agent
95
+ end
96
+
97
+ def set_gzip(request)
98
+ request["Accept-Encoding"] = "gzip,deflate"
99
+ end
100
+
101
+ def timeout_exception
102
+ defined?(Net::ReadTimeout) ? Net::ReadTimeout : Timeout::Error
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,74 @@
1
+ module Aitch
2
+ class Response
3
+ extend Forwardable
4
+
5
+ def_delegators :@http_response, :content_type
6
+
7
+ def initialize(http_response)
8
+ @http_response = http_response
9
+ end
10
+
11
+ def code
12
+ @http_response.code.to_i
13
+ end
14
+
15
+ def body
16
+ @body ||= Body.new(@http_response).to_s
17
+ end
18
+
19
+ def success?
20
+ code >= 200 && code <= 399
21
+ end
22
+
23
+ def redirect?
24
+ code >= 300 && code <= 399
25
+ end
26
+
27
+ def error?
28
+ code >= 400 && code <= 599
29
+ end
30
+
31
+ def error
32
+ error? && ERRORS.fetch(code)
33
+ end
34
+
35
+ def json?
36
+ content_type =~ /json/
37
+ end
38
+
39
+ def xml?
40
+ content_type =~ /xml/
41
+ end
42
+
43
+ def html?
44
+ content_type =~ /html/
45
+ end
46
+
47
+ def data
48
+ if json?
49
+ Aitch.configuration.json_parser.load(body)
50
+ elsif xml?
51
+ Aitch.configuration.xml_parser.load(body)
52
+ else
53
+ body
54
+ end
55
+ end
56
+
57
+ def headers
58
+ @headers ||= {}.tap do |headers|
59
+ @http_response.each_header do |name, value|
60
+ headers[name.gsub(/^x-/, "")] = value
61
+ end
62
+ end
63
+ end
64
+
65
+ def method_missing(name, *args, &block)
66
+ return headers[name.to_s] if headers.key?(name.to_s)
67
+ super
68
+ end
69
+
70
+ def respond_to_missing?(name, include_private = false)
71
+ headers.key?(name.to_s)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,29 @@
1
+ module Aitch
2
+ class Response
3
+ class Body
4
+ def initialize(response)
5
+ @response = response
6
+ @body = response.body
7
+ @encoding = @response["content-encoding"]
8
+ end
9
+
10
+ def gzip?
11
+ @encoding == "gzip"
12
+ end
13
+
14
+ def deflate?
15
+ @encoding == "deflate"
16
+ end
17
+
18
+ def to_s
19
+ if gzip?
20
+ Zlib::GzipReader.new(StringIO.new(@body)).read
21
+ elsif deflate?
22
+ Zlib::Inflate.inflate(@body)
23
+ else
24
+ @body
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,39 @@
1
+ module Aitch
2
+ class Response
3
+ ERRORS = {
4
+ 400 => BadRequestError,
5
+ 401 => UnauthorizedError,
6
+ 402 => PaymentRequiredError,
7
+ 403 => ForbiddenError,
8
+ 404 => NotFoundError,
9
+ 405 => MethodNotAllowedError,
10
+ 406 => NotAcceptableError,
11
+ 407 => ProxyAuthenticationRequiredError,
12
+ 408 => RequestTimeOutError,
13
+ 409 => ConflictError,
14
+ 410 => GoneError,
15
+ 411 => LengthRequiredError,
16
+ 412 => PreconditionFailedError,
17
+ 413 => RequestEntityTooLargeError,
18
+ 414 => RequestURITooLongError,
19
+ 415 => UnsupportedMediaTypeError,
20
+ 416 => RequestedRangeNotSatisfiableError,
21
+ 417 => ExpectationFailedError,
22
+ 422 => UnprocessableEntityError,
23
+ 423 => LockedError,
24
+ 424 => FailedDependencyError,
25
+ 426 => UpgradeRequiredError,
26
+ 428 => PreconditionRequiredError,
27
+ 429 => TooManyRequestsError,
28
+ 431 => RequestHeaderFieldsTooLargeError,
29
+ 500 => InternalServerErrorError,
30
+ 501 => NotImplementedError,
31
+ 502 => BadGatewayError,
32
+ 503 => ServiceUnavailableError,
33
+ 504 => GatewayTimeOutError,
34
+ 505 => VersionNotSupportedError,
35
+ 507 => InsufficientStorageError,
36
+ 511 => NetworkAuthenticationRequiredError
37
+ }
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module Aitch
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ module Aitch
2
+ module XMLParser
3
+ def self.load(source)
4
+ Nokogiri::XML(source.to_s)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ describe Aitch do
4
+ context "request methods" do
5
+ it { should respond_to(:get) }
6
+ it { should respond_to(:get!) }
7
+ it { should respond_to(:post) }
8
+ it { should respond_to(:post!) }
9
+ it { should respond_to(:put) }
10
+ it { should respond_to(:put!) }
11
+ it { should respond_to(:patch) }
12
+ it { should respond_to(:patch!) }
13
+ it { should respond_to(:delete) }
14
+ it { should respond_to(:delete!) }
15
+ it { should respond_to(:head) }
16
+ it { should respond_to(:head!) }
17
+ it { should respond_to(:options) }
18
+ it { should respond_to(:options!) }
19
+ it { should respond_to(:trace) }
20
+ it { should respond_to(:trace!) }
21
+ it { should respond_to(:execute) }
22
+ it { should respond_to(:execute!) }
23
+ end
24
+
25
+ describe "#execute" do
26
+ let(:request) { mock.as_null_object }
27
+
28
+ it "delegates to Request" do
29
+ Aitch::Request
30
+ .should_receive(:new)
31
+ .with("METHOD", "URL", "ARGS", "HEADERS", "OPTIONS")
32
+ .and_return(request)
33
+
34
+ Aitch.execute("METHOD", "URL", "ARGS", "HEADERS", "OPTIONS")
35
+ end
36
+
37
+ it "performs request" do
38
+ Aitch::Request.stub new: request
39
+ request.should_receive(:perform)
40
+
41
+ Aitch.execute("METHOD", "URL")
42
+ end
43
+ end
44
+
45
+ describe "#execute!" do
46
+ it "returns response when successful" do
47
+ response = stub(error?: false)
48
+ Aitch::Request.any_instance.stub perform: response
49
+
50
+ expect(Aitch.execute!("METHOD", "URL")).to eql(response)
51
+ end
52
+
53
+ it "raises when has errors" do
54
+ response = stub(error?: true, error: "ERROR")
55
+ Aitch::Request.any_instance.stub perform: response
56
+
57
+ expect {
58
+ Aitch.execute!("METHOD", "URL")
59
+ }.to raise_error("ERROR")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Aitch::Configuration do
4
+ it "sets default timeout" do
5
+ expect(Aitch::Configuration.new.timeout).to eql(10)
6
+ end
7
+
8
+ it "sets default user agent" do
9
+ user_agent = "Aitch/#{Aitch::VERSION} (http://rubygems.org/gems/aitch)"
10
+ expect(Aitch::Configuration.new.user_agent).to eql(user_agent)
11
+ end
12
+
13
+ it "sets default maximum redirections" do
14
+ expect(Aitch::Configuration.new.redirect_limit).to eql(5)
15
+ end
16
+
17
+ it "sets default headers" do
18
+ expect(Aitch::Configuration.new.default_headers).to eql({})
19
+ end
20
+
21
+ it "sets default XML parser" do
22
+ expect(Aitch::Configuration.new.xml_parser).to eql(Aitch::XMLParser)
23
+ end
24
+
25
+ it "configures aitch" do
26
+ Aitch.configure do |config|
27
+ config.timeout = 15
28
+ end
29
+
30
+ expect(Aitch.configuration.timeout).to eql(15)
31
+ end
32
+ end
33
+
@@ -0,0 +1,155 @@
1
+ require "spec_helper"
2
+
3
+ describe Aitch::Request do
4
+ it "raises with invalid uri" do
5
+ expect {
6
+ Aitch::Request.new("post", "\\").uri
7
+ }.to raise_error(Aitch::InvalidURIError)
8
+ end
9
+
10
+ it "raises on timeout", ruby: 2.0 do
11
+ request = Aitch::Request.new("post", "http://example.org")
12
+ request.stub_chain(:client, :request).and_raise(Net::ReadTimeout)
13
+
14
+ expect {
15
+ request.perform
16
+ }.to raise_error(Aitch::RequestTimeoutError)
17
+ end
18
+
19
+ it "raises on timeout", ruby: 1.9 do
20
+ request = Aitch::Request.new("post", "http://example.org")
21
+ request.stub_chain(:client, :request).and_raise(Timeout::Error)
22
+
23
+ expect {
24
+ request.perform
25
+ }.to raise_error(Aitch::RequestTimeoutError)
26
+ end
27
+
28
+ it "sets user agent" do
29
+ request = Aitch::Request.new("post", "http://example.org/some/path").request
30
+ expect(request["User-Agent"]).to eql(Aitch.configuration.user_agent)
31
+ end
32
+
33
+ it "requests gzip encoding" do
34
+ request = Aitch::Request.new("get", "http://example.org").request
35
+ expect(request["Accept-Encoding"]).to eql("gzip,deflate")
36
+ end
37
+
38
+ it "sets path" do
39
+ request = Aitch::Request.new("post", "http://example.org/some/path").request
40
+ expect(request.path).to eql("/some/path")
41
+ end
42
+
43
+ it "sets request body from hash" do
44
+ request = Aitch::Request.new("post", "http://example.org/", {a: 1}).request
45
+ expect(request.body).to eql("a=1")
46
+ end
47
+
48
+ it "sets request body from string" do
49
+ request = Aitch::Request.new("post", "http://example.org/", "some body").request
50
+ expect(request.body).to eql("some body")
51
+ end
52
+
53
+ it "sets request body from to_h protocol" do
54
+ data = stub(to_h: {a: 1})
55
+ request = Aitch::Request.new("post", "http://example.org/", data).request
56
+ expect(request.body).to eql("a=1")
57
+ end
58
+
59
+ it "sets request body from to_s protocol" do
60
+ data = stub(to_s: "some body")
61
+ request = Aitch::Request.new("post", "http://example.org/", data).request
62
+
63
+ expect(request.body).to eql("some body")
64
+ end
65
+
66
+ it "sets default headers" do
67
+ Aitch.configuration.default_headers = {"HEADER" => "VALUE"}
68
+ request = Aitch::Request.new("post", "http://example.org/").request
69
+ expect(request["HEADER"]).to eql("VALUE")
70
+ end
71
+
72
+ it "sets custom headers" do
73
+ request = Aitch::Request.new("post", "http://example.org/", {}, {"HEADER" => "VALUE"}).request
74
+ expect(request["HEADER"]).to eql("VALUE")
75
+ end
76
+
77
+ it "sets basic auth credentials" do
78
+ request = Aitch::Request.new("post", "http://example.org/", {}, {}, {user: "USER", password: "PASS"}).request
79
+ credentials = Base64.decode64(request["Authorization"].gsub(/Basic /, ""))
80
+
81
+ expect(credentials).to eql("USER:PASS")
82
+ end
83
+
84
+ describe "#client" do
85
+ context "https" do
86
+ subject(:client) {
87
+ Aitch::Request.new("get", "https://example.org").client
88
+ }
89
+
90
+ it "sets https" do
91
+ expect(client.use_ssl?).to be_true
92
+ end
93
+
94
+ it "sets verification mode" do
95
+ expect(client.verify_mode).to eql(OpenSSL::SSL::VERIFY_PEER)
96
+ end
97
+
98
+ it "sets timeout" do
99
+ Aitch.configuration.timeout = 20
100
+ expect(client.read_timeout).to eql(20)
101
+ end
102
+ end
103
+ end
104
+
105
+ describe "Request class" do
106
+ it "raises with invalid method" do
107
+ expect {
108
+ Aitch::Request.new("invalid", "URL").request
109
+ }.to raise_error(Aitch::InvalidHTTPMethodError, %[unexpected HTTP verb: "invalid"])
110
+ end
111
+
112
+ %w[
113
+ get
114
+ post
115
+ put
116
+ patch
117
+ delete
118
+ head
119
+ options
120
+ trace
121
+ ].each do |method|
122
+ it "instantiates #{method.upcase} method" do
123
+ request = Aitch::Request.new(method, "URL").request
124
+ expect(request.class.name).to eql("Net::HTTP::#{method.capitalize}")
125
+ end
126
+ end
127
+ end
128
+
129
+ describe "follow redirection" do
130
+ before { Aitch.configuration.follow_redirect = true }
131
+
132
+ it "follows redirect" do
133
+ Aitch.configuration.redirect_limit = 5
134
+
135
+ FakeWeb.register_uri(:get, "http://example.org/", location: "http://example.com/", status: 301)
136
+ FakeWeb.register_uri(:get, "http://example.com/", body: "Hello")
137
+
138
+ response = Aitch.get("http://example.org")
139
+
140
+ expect(response).not_to be_redirect
141
+ expect(response.body).to eql("Hello")
142
+ end
143
+
144
+ it "raises when doing too many redirects" do
145
+ Aitch.configuration.redirect_limit = 1
146
+
147
+ FakeWeb.register_uri(:get, "http://example.org/", location: "http://example.com/", status: 301)
148
+ FakeWeb.register_uri(:get, "http://example.com/", location: "https://example.com/", status: 301)
149
+
150
+ expect {
151
+ Aitch.get("http://example.org/")
152
+ }.to raise_error(Aitch::TooManyRedirectsError)
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,180 @@
1
+ require "spec_helper"
2
+
3
+ describe Aitch::Response do
4
+ it "has body" do
5
+ FakeWeb.register_uri(:get, "http://example.org/", body: "Hello")
6
+ response = Aitch.get("http://example.org/")
7
+ expect(response.body).to eql("Hello")
8
+ end
9
+
10
+ it "parses gzip response" do
11
+ stdio = StringIO.new
12
+ gzipped = Zlib::GzipWriter.new(stdio)
13
+ gzipped.write("Hello")
14
+ gzipped.finish
15
+
16
+ FakeWeb.register_uri(:get, "http://example.org/", body: stdio.string, content_encoding: "gzip")
17
+ response = Aitch.get("http://example.org/")
18
+
19
+ expect(response.body).to eql("Hello")
20
+ end
21
+
22
+ it "deflates response" do
23
+ stdio = StringIO.new
24
+ deflated = Zlib::Deflate.deflate("Hello")
25
+
26
+ FakeWeb.register_uri(:get, "http://example.org/", body: deflated, content_encoding: "deflate")
27
+ response = Aitch.get("http://example.org/")
28
+
29
+ expect(response.body).to eql("Hello")
30
+ end
31
+
32
+ it "returns status code" do
33
+ FakeWeb.register_uri(:get, "http://example.org/", body: "")
34
+ response = Aitch.get("http://example.org/")
35
+ expect(response.code).to eql(200)
36
+ end
37
+
38
+ it "returns content type" do
39
+ FakeWeb.register_uri(:get, "http://example.org/", content_type: "text/html")
40
+ response = Aitch.get("http://example.org/")
41
+ expect(response.content_type).to eql("text/html")
42
+ end
43
+
44
+ it "detects as successful response" do
45
+ FakeWeb.register_uri(:get, "http://example.org/", content_type: "text/html")
46
+ response = Aitch.get("http://example.org/")
47
+ expect(response).to be_success
48
+ end
49
+
50
+ it "returns headers" do
51
+ FakeWeb.register_uri(:get, "http://example.org/", content_type: "text/html")
52
+ headers = Aitch.get("http://example.org/").headers
53
+
54
+ expect(headers).to be_a(Hash)
55
+ expect(headers["content-type"]).to eql("text/html")
56
+ end
57
+
58
+ it "normalizes custom headers" do
59
+ FakeWeb.register_uri(:get, "http://example.org/", "X-Runtime" => "0.003")
60
+ headers = Aitch.get("http://example.org/").headers
61
+
62
+ expect(headers["runtime"]).to eql("0.003")
63
+ end
64
+
65
+ it "maps missing methods to headers" do
66
+ FakeWeb.register_uri(:get, "http://example.org/", "X-Runtime" => "0.003")
67
+ response = Aitch.get("http://example.org/")
68
+
69
+ expect(response.runtime).to eql("0.003")
70
+ expect(response).to respond_to(:runtime)
71
+ end
72
+
73
+ context "status 3xx" do
74
+ before { Aitch.configuration.follow_redirect = false }
75
+
76
+ it "has body" do
77
+ FakeWeb.register_uri(:get, "http://example.org/", body: "Hello", status: 301)
78
+ response = Aitch.get("http://example.org/")
79
+ expect(response.body).to eql("Hello")
80
+ end
81
+
82
+ it "detects as successful response" do
83
+ FakeWeb.register_uri(:get, "http://example.org/", status: 301)
84
+ response = Aitch.get("http://example.org/")
85
+ expect(response).to be_success
86
+ end
87
+
88
+ it "detects as redirect" do
89
+ FakeWeb.register_uri(:get, "http://example.org/", status: 301)
90
+ response = Aitch.get("http://example.org/")
91
+ expect(response).to be_redirect
92
+ end
93
+
94
+ it "returns location" do
95
+ FakeWeb.register_uri(:get, "http://example.org/", status: 301, location: "https://example.com/")
96
+ response = Aitch.get("http://example.org/")
97
+ expect(response.location).to eql("https://example.com/")
98
+ end
99
+ end
100
+
101
+ context "status 4xx" do
102
+ it "detects as error" do
103
+ FakeWeb.register_uri(:get, "http://example.org/", status: 404)
104
+ response = Aitch.get("http://example.org/")
105
+
106
+ expect(response).to be_error
107
+ end
108
+
109
+ it "sets error" do
110
+ FakeWeb.register_uri(:get, "http://example.org/", status: 404)
111
+ response = Aitch.get("http://example.org/")
112
+
113
+ expect(response.error).to eql(Aitch::NotFoundError)
114
+ end
115
+ end
116
+
117
+ context "status 5xx" do
118
+ it "detects as error" do
119
+ FakeWeb.register_uri(:get, "http://example.org/", status: 500)
120
+ response = Aitch.get("http://example.org/")
121
+
122
+ expect(response).to be_error
123
+ end
124
+
125
+ it "sets error" do
126
+ FakeWeb.register_uri(:get, "http://example.org/", status: 500)
127
+ response = Aitch.get("http://example.org/")
128
+
129
+ expect(response.error).to eql(Aitch::InternalServerErrorError)
130
+ end
131
+ end
132
+
133
+ context "JSON" do
134
+ it "detects as json" do
135
+ FakeWeb.register_uri(:get, "http://example.org/", body: "[]", content_type: "application/json")
136
+ response = Aitch.get("http://example.org/")
137
+
138
+ expect(response).to be_json
139
+ end
140
+
141
+ it "returns data" do
142
+ FakeWeb.register_uri(:get, "http://example.org/", body: "[1,2,3]", content_type: "application/json")
143
+ response = Aitch.get("http://example.org/")
144
+
145
+ expect(response.data).to eql([1,2,3])
146
+ end
147
+ end
148
+
149
+ context "HTML" do
150
+ it "detects as html" do
151
+ FakeWeb.register_uri(:get, "http://example.org/", body: "", content_type: "text/html")
152
+ response = Aitch.get("http://example.org/")
153
+
154
+ expect(response).to be_html
155
+ end
156
+
157
+ it "returns data" do
158
+ FakeWeb.register_uri(:get, "http://example.org/", body: "Hello", content_type: "text/html")
159
+ response = Aitch.get("http://example.org/")
160
+
161
+ expect(response.data).to eql("Hello")
162
+ end
163
+ end
164
+
165
+ context "XML" do
166
+ it "detects as xml" do
167
+ FakeWeb.register_uri(:get, "http://example.org/", body: "[]", content_type: "application/xml")
168
+ response = Aitch.get("http://example.org/")
169
+
170
+ expect(response).to be_xml
171
+ end
172
+
173
+ it "returns data" do
174
+ FakeWeb.register_uri(:get, "http://example.org/", body: "<foo/>", content_type: "application/xml")
175
+ response = Aitch.get("http://example.org/")
176
+
177
+ expect(response.data).to be_a(Nokogiri::XML::Document)
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,11 @@
1
+ require "spec_helper"
2
+
3
+ describe Aitch::XMLParser do
4
+ it "instantiates Nokogiri" do
5
+ Nokogiri
6
+ .should_receive(:XML)
7
+ .with("XML")
8
+
9
+ Aitch::XMLParser.load("XML")
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ require "bundler/setup"
2
+ Bundler.require
3
+
4
+ require "aitch"
5
+ require "base64"
6
+ require "test_notifier/runner/rspec"
7
+ require "fakeweb"
8
+ require "nokogiri"
9
+
10
+ FakeWeb.allow_net_connect = false
11
+
12
+ RSpec.configure do |config|
13
+ config.filter_run_excluding :ruby => -> version {
14
+ !(RUBY_VERSION.to_s =~ /^#{version.to_s}/)
15
+ }
16
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aitch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Nando Vieira
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry-meta
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: test_notifier
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: fakeweb
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: nokogiri
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: A simple HTTP client
127
+ email:
128
+ - fnando.vieira@gmail.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - .gitignore
134
+ - .rspec
135
+ - .travis.yml
136
+ - Gemfile
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - aitch.gemspec
141
+ - lib/aitch.rb
142
+ - lib/aitch/configuration.rb
143
+ - lib/aitch/errors.rb
144
+ - lib/aitch/redirect.rb
145
+ - lib/aitch/request.rb
146
+ - lib/aitch/response.rb
147
+ - lib/aitch/response/body.rb
148
+ - lib/aitch/response/errors.rb
149
+ - lib/aitch/version.rb
150
+ - lib/aitch/xml_parser.rb
151
+ - spec/aitch/aitch_spec.rb
152
+ - spec/aitch/configuration_spec.rb
153
+ - spec/aitch/request_spec.rb
154
+ - spec/aitch/response_spec.rb
155
+ - spec/aitch/xml_parser_spec.rb
156
+ - spec/spec_helper.rb
157
+ homepage: http://rubygems.org/gems/aitch
158
+ licenses:
159
+ - MIT
160
+ post_install_message:
161
+ rdoc_options: []
162
+ require_paths:
163
+ - lib
164
+ required_ruby_version: !ruby/object:Gem::Requirement
165
+ none: false
166
+ requirements:
167
+ - - ! '>='
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ required_rubygems_version: !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ! '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ requirements: []
177
+ rubyforge_project:
178
+ rubygems_version: 1.8.23
179
+ signing_key:
180
+ specification_version: 3
181
+ summary: A simple HTTP client
182
+ test_files:
183
+ - spec/aitch/aitch_spec.rb
184
+ - spec/aitch/configuration_spec.rb
185
+ - spec/aitch/request_spec.rb
186
+ - spec/aitch/response_spec.rb
187
+ - spec/aitch/xml_parser_spec.rb
188
+ - spec/spec_helper.rb