weary 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +9 -4
- data/Gemfile.lock +36 -20
- data/README.md +11 -3
- data/lib/weary/adapter.rb +25 -1
- data/lib/weary/adapters/excon.rb +36 -0
- data/lib/weary/adapters/net_http.rb +29 -47
- data/lib/weary/adapters/typhoeus.rb +32 -0
- data/lib/weary/client.rb +2 -29
- data/lib/weary/deferred.rb +1 -1
- data/lib/weary/env.rb +6 -1
- data/lib/weary/middleware/timeout.rb +22 -0
- data/lib/weary/request.rb +14 -23
- data/lib/weary/requestable.rb +69 -0
- data/lib/weary/resource.rb +35 -32
- data/lib/weary/response.rb +6 -2
- data/lib/weary/route.rb +6 -1
- data/lib/weary/version.rb +1 -1
- data/spec/spec_helper.rb +2 -1
- data/spec/support/shared_examples_for_a_rack_app.rb +12 -0
- data/spec/support/shared_examples_for_a_requestable.rb +77 -0
- data/spec/support/shared_examples_for_an_adapter.rb +16 -0
- data/spec/weary/adapter_spec.rb +8 -15
- data/spec/weary/adapters/excon_spec.rb +86 -0
- data/spec/weary/adapters/nethttp_spec.rb +63 -54
- data/spec/weary/adapters/typhoeus_spec.rb +68 -0
- data/spec/weary/client_spec.rb +4 -0
- data/spec/weary/deferred_spec.rb +1 -1
- data/spec/weary/middleware/basic_auth_spec.rb +2 -2
- data/spec/weary/middleware/timeout_spec.rb +24 -0
- data/spec/weary/request_spec.rb +6 -42
- data/spec/weary/requestable_spec.rb +9 -0
- data/spec/weary/resource_spec.rb +6 -18
- data/spec/weary/response_spec.rb +10 -0
- data/weary.gemspec +2 -2
- metadata +56 -15
data/.gitignore
CHANGED
data/Gemfile
CHANGED
@@ -3,17 +3,22 @@ source "http://rubygems.org"
|
|
3
3
|
gemspec
|
4
4
|
|
5
5
|
gem "rake", "~> 0.9.2"
|
6
|
+
gem "excon"
|
6
7
|
|
7
8
|
group :test do
|
8
|
-
gem "rspec", "~> 2.
|
9
|
-
gem "webmock", "~> 1.8.
|
9
|
+
gem "rspec", "~> 2.11.0"
|
10
|
+
gem "webmock", "~> 1.8.10"
|
10
11
|
end
|
11
12
|
|
12
13
|
group :doc do
|
13
|
-
gem "yard", "~> 0.
|
14
|
-
gem "yard-tomdoc", "~> 0.
|
14
|
+
gem "yard", "~> 0.8.2"
|
15
|
+
gem "yard-tomdoc", "~> 0.5.0"
|
15
16
|
end
|
16
17
|
|
17
18
|
platforms :jruby do
|
18
19
|
gem "jruby-openssl"
|
19
20
|
end
|
21
|
+
|
22
|
+
platforms :mri, :jruby do
|
23
|
+
gem "typhoeus"
|
24
|
+
end
|
data/Gemfile.lock
CHANGED
@@ -2,8 +2,8 @@ PATH
|
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
4
|
weary (1.0.1)
|
5
|
-
addressable (~> 2.2
|
6
|
-
multi_json (~> 1.
|
5
|
+
addressable (~> 2.3.2)
|
6
|
+
multi_json (~> 1.3.0)
|
7
7
|
promise (~> 0.3.0)
|
8
8
|
rack (~> 1.4.0)
|
9
9
|
simple_oauth (~> 0.1.5)
|
@@ -11,28 +11,42 @@ PATH
|
|
11
11
|
GEM
|
12
12
|
remote: http://rubygems.org/
|
13
13
|
specs:
|
14
|
-
addressable (2.2
|
14
|
+
addressable (2.3.2)
|
15
|
+
courtier (0.2.0)
|
16
|
+
finder
|
17
|
+
loaded
|
15
18
|
crack (0.3.1)
|
16
19
|
diff-lcs (1.1.3)
|
17
|
-
|
20
|
+
excon (0.16.2)
|
21
|
+
ffi (1.1.5)
|
22
|
+
finder (0.3.0)
|
23
|
+
loaded (0.0.1)
|
24
|
+
mime-types (1.19)
|
25
|
+
multi_json (1.3.6)
|
18
26
|
promise (0.3.0)
|
19
27
|
rack (1.4.1)
|
20
28
|
rake (0.9.2.2)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
rspec-
|
25
|
-
|
26
|
-
|
29
|
+
rc (0.2.0)
|
30
|
+
courtier (= 0.2.0)
|
31
|
+
rspec (2.11.0)
|
32
|
+
rspec-core (~> 2.11.0)
|
33
|
+
rspec-expectations (~> 2.11.0)
|
34
|
+
rspec-mocks (~> 2.11.0)
|
35
|
+
rspec-core (2.11.1)
|
36
|
+
rspec-expectations (2.11.3)
|
27
37
|
diff-lcs (~> 1.1.3)
|
28
|
-
rspec-mocks (2.
|
29
|
-
simple_oauth (0.1.
|
30
|
-
tomparse (0.2.
|
31
|
-
|
38
|
+
rspec-mocks (2.11.2)
|
39
|
+
simple_oauth (0.1.9)
|
40
|
+
tomparse (0.2.1)
|
41
|
+
typhoeus (0.4.2)
|
42
|
+
ffi (~> 1.0)
|
43
|
+
mime-types (~> 1.18)
|
44
|
+
webmock (1.8.10)
|
32
45
|
addressable (>= 2.2.7)
|
33
46
|
crack (>= 0.1.7)
|
34
|
-
yard (0.
|
35
|
-
yard-tomdoc (0.
|
47
|
+
yard (0.8.2.1)
|
48
|
+
yard-tomdoc (0.5.0)
|
49
|
+
rc
|
36
50
|
tomparse
|
37
51
|
yard
|
38
52
|
|
@@ -40,10 +54,12 @@ PLATFORMS
|
|
40
54
|
ruby
|
41
55
|
|
42
56
|
DEPENDENCIES
|
57
|
+
excon
|
43
58
|
jruby-openssl
|
44
59
|
rake (~> 0.9.2)
|
45
|
-
rspec (~> 2.
|
60
|
+
rspec (~> 2.11.0)
|
61
|
+
typhoeus
|
46
62
|
weary!
|
47
|
-
webmock (~> 1.8.
|
48
|
-
yard (~> 0.
|
49
|
-
yard-tomdoc (~> 0.
|
63
|
+
webmock (~> 1.8.10)
|
64
|
+
yard (~> 0.8.2)
|
65
|
+
yard-tomdoc (~> 0.5.0)
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Weary
|
2
2
|
|
3
|
+
[![Build Status](https://secure.travis-ci.org/mwunsch/weary.png)](http://travis-ci.org/mwunsch/weary) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/mwunsch/weary)
|
4
|
+
|
3
5
|
_Weary is a framework and DSL for building clients for (preferably RESTful) web service APIs._
|
4
6
|
|
5
7
|
At its most minimal, Weary is simply some nice syntactic sugar around Net/HTTP.
|
@@ -56,7 +58,7 @@ end
|
|
56
58
|
|
57
59
|
The DSL provides methods for all of the HTTP verbs (See `Weary::Client::REQUEST_METHODS`). When you instantiate this class, the object will have an instance method named "resource" that will return a `Weary::Request` object set up to perform a "GET" request on "http://host.com/path/to/resource".
|
58
60
|
|
59
|
-
You can pass a block these methods for access to the `Weary::Resource`.
|
61
|
+
You can pass a block to these methods for access to the `Weary::Resource`.
|
60
62
|
|
61
63
|
Further methods in the DSL include:
|
62
64
|
|
@@ -113,9 +115,15 @@ The method that the Client defines (in the above example, the `client.update` me
|
|
113
115
|
|
114
116
|
### Weary::Request
|
115
117
|
|
116
|
-
No matter how you get there, you'll end up with a Weary::Request object. Call the `perform` method to actually make the request and get back a `Weary::Response`. That's not entirely true `Weary::Request#perform` is asynchronous and non-blocking. It returns a future and will only block once you call a method on the response. You can optionally pass a block that's executed once the response has returned.
|
118
|
+
No matter how you get there, you'll end up with a Weary::Request object. Call the `perform` method to actually make the request and get back a `Weary::Response`. That's not entirely true... `Weary::Request#perform` is asynchronous and non-blocking. It returns a future and will only block once you call a method on the response. You can optionally pass a block that's executed once the response has returned.
|
119
|
+
|
120
|
+
By default, the request is performed through [Net::HTTP](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html). This is done through `Weary::Adapter::NetHttp`. A `Weary::Adapter` is just a special kind of Rack application. `Request#adapter` allows you to hook up your own. Weary also includes adapters for [Typhoeus](http://typhoeus.github.com/) and [Excon](https://github.com/geemus/excon).
|
121
|
+
|
122
|
+
#### Requestable
|
123
|
+
|
124
|
+
`Client`, `Resource`, and `Request` include a Module named `Requestable`. Using this module, it's easy to cascade certain pieces of configuration down from the stack.
|
117
125
|
|
118
|
-
|
126
|
+
For example, you can call `Client#adapter` to change the adapter for all of the resources of that client. Or you can call `Resource#adapter` to change the adapter for requests built for that resource. OR you can call `Request#adapter` to change the adapter for just that request.
|
119
127
|
|
120
128
|
## Rack
|
121
129
|
|
data/lib/weary/adapter.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
require 'weary/response'
|
2
|
-
|
2
|
+
|
3
3
|
module Weary
|
4
4
|
# An abstract interface. A subclass should be something that actually opens
|
5
5
|
# a socket to make the request, e.g. Net/HTTP, Curb, etc.
|
6
6
|
module Adapter
|
7
|
+
autoload :NetHttp, 'weary/adapters/net_http'
|
8
|
+
autoload :Excon, 'weary/adapters/excon'
|
9
|
+
autoload :Typhoeus, 'weary/adapters/typhoeus'
|
7
10
|
|
8
11
|
def initialize(app=nil)
|
9
12
|
@app = app
|
@@ -19,5 +22,26 @@ module Weary
|
|
19
22
|
def connect(request)
|
20
23
|
Rack::Response.new [""], 501, {"Content-Type" => "text/plain"}
|
21
24
|
end
|
25
|
+
|
26
|
+
# Modify the headers of an Env to be Capitalized strings with dashes (as
|
27
|
+
# opposed to the CGI-like headers needed by Rack).
|
28
|
+
def normalize_request_headers(env)
|
29
|
+
req_headers = env.reject {|k,v| !k.start_with? "HTTP_" }
|
30
|
+
normalized = req_headers.map do |k, v|
|
31
|
+
new_key = k.sub("HTTP_",'').split('_').map(&:capitalize).join('-')
|
32
|
+
[new_key, v] unless UNWANTED_REQUEST_HEADERS.include? new_key
|
33
|
+
end
|
34
|
+
Hash[normalized]
|
35
|
+
end
|
36
|
+
|
37
|
+
# According to the Rack Spec:
|
38
|
+
# > The header must not contain a Status key...
|
39
|
+
def normalize_response(headers)
|
40
|
+
headers.reject {|k,v| k.downcase == 'status' }
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
UNWANTED_REQUEST_HEADERS = []
|
22
46
|
end
|
23
47
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'excon'
|
2
|
+
|
3
|
+
module Weary
|
4
|
+
module Adapter
|
5
|
+
class Excon
|
6
|
+
|
7
|
+
class << self
|
8
|
+
include Weary::Adapter
|
9
|
+
|
10
|
+
def connect(request)
|
11
|
+
connection = ::Excon.new("#{request.scheme}://#{request.host_with_port}")
|
12
|
+
response = connection.request prepare(request)
|
13
|
+
Rack::Response.new response.body, response.status, normalize_response(response.headers)
|
14
|
+
end
|
15
|
+
|
16
|
+
def prepare(request)
|
17
|
+
has_query = !(request.query_string.nil? || request.query_string.empty?)
|
18
|
+
excon_params = { :headers => normalize_request_headers(request.env),
|
19
|
+
:method => request.request_method,
|
20
|
+
:path => request.path,
|
21
|
+
:body => request.body.read }
|
22
|
+
excon_params[:query] if has_query
|
23
|
+
request.body.rewind
|
24
|
+
excon_params
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
include Weary::Adapter
|
29
|
+
|
30
|
+
def connect(rack_request)
|
31
|
+
self.class.connect(rack_request)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -4,65 +4,47 @@ require 'net/https'
|
|
4
4
|
module Weary
|
5
5
|
module Adapter
|
6
6
|
class NetHttp
|
7
|
-
include Weary::Adapter
|
8
|
-
|
9
|
-
def self.call(env)
|
10
|
-
connect(Rack::Request.new(env)).finish
|
11
|
-
end
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
response = connection.request prepare(request)
|
16
|
-
Rack::Response.new response.body, response.code, normalize_response(response.to_hash)
|
17
|
-
end
|
8
|
+
class << self
|
9
|
+
include Weary::Adapter
|
18
10
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
req.body = request.body.read
|
24
|
-
request.body.rewind
|
11
|
+
def connect(request)
|
12
|
+
connection = socket(request)
|
13
|
+
response = connection.request prepare(request)
|
14
|
+
Rack::Response.new response.body || "", response.code, normalize_response(response.to_hash)
|
25
15
|
end
|
26
|
-
req.content_type = request.content_type if request.content_type
|
27
|
-
req.content_length = request.content_length if request.content_length
|
28
|
-
req
|
29
|
-
end
|
30
16
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
17
|
+
def prepare(request)
|
18
|
+
req_class = request_class(request.request_method)
|
19
|
+
net_http_req = req_class.new(request.fullpath, normalize_request_headers(request.env))
|
20
|
+
if net_http_req.request_body_permitted? # What's the best way of passing the body?
|
21
|
+
net_http_req.body = request.body.read
|
22
|
+
request.body.rewind
|
23
|
+
end
|
24
|
+
net_http_req.content_type = request.content_type if request.content_type
|
25
|
+
net_http_req.content_length = request.content_length if request.content_length
|
26
|
+
net_http_req
|
36
27
|
end
|
37
|
-
Hash[normalized]
|
38
|
-
end
|
39
28
|
|
40
|
-
|
41
|
-
|
42
|
-
|
29
|
+
def socket(request)
|
30
|
+
connection = Net::HTTP.new request.host, request.port.to_s
|
31
|
+
connection.use_ssl = request.scheme == 'https'
|
32
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if connection.use_ssl?
|
33
|
+
connection
|
34
|
+
end
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
connection.use_ssl = request.scheme == 'https'
|
49
|
-
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if connection.use_ssl?
|
50
|
-
connection
|
36
|
+
def request_class(method)
|
37
|
+
capitalized = method.capitalize
|
38
|
+
Net::HTTP.const_get capitalized
|
39
|
+
end
|
51
40
|
end
|
52
41
|
|
53
|
-
|
54
|
-
capitalized = method.capitalize
|
55
|
-
Net::HTTP.const_get capitalized
|
56
|
-
end
|
42
|
+
include Weary::Adapter
|
57
43
|
|
58
|
-
def
|
59
|
-
self.class.
|
44
|
+
def connect(rack_request)
|
45
|
+
self.class.connect(rack_request)
|
60
46
|
end
|
61
47
|
|
62
|
-
private
|
63
|
-
|
64
|
-
UNWANTED_REQUEST_HEADERS = []
|
65
|
-
|
66
48
|
end
|
67
49
|
end
|
68
50
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'typhoeus'
|
2
|
+
|
3
|
+
module Weary
|
4
|
+
module Adapter
|
5
|
+
class Typhoeus
|
6
|
+
|
7
|
+
class << self
|
8
|
+
include Weary::Adapter
|
9
|
+
|
10
|
+
def connect(rack_request)
|
11
|
+
response = ::Typhoeus::Request.run(rack_request.url, parameters_for(rack_request))
|
12
|
+
Rack::Response.new response.body, response.code, response.headers_hash
|
13
|
+
end
|
14
|
+
|
15
|
+
def parameters_for(request)
|
16
|
+
typhoeus_params = { :headers => normalize_request_headers(request.env),
|
17
|
+
:method => request.request_method.downcase.to_sym,
|
18
|
+
:body => request.body.read }
|
19
|
+
request.body.rewind
|
20
|
+
typhoeus_params
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
include Weary::Adapter
|
25
|
+
|
26
|
+
def connect(rack_request)
|
27
|
+
self.class.connect(rack_request)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/weary/client.rb
CHANGED
@@ -47,6 +47,7 @@ module Weary
|
|
47
47
|
]
|
48
48
|
|
49
49
|
class << self
|
50
|
+
include Weary::Requestable
|
50
51
|
|
51
52
|
REQUEST_METHODS.each do |request_method|
|
52
53
|
# Generate a resource of the specified REQUEST_METHOD. This
|
@@ -116,31 +117,6 @@ module Weary
|
|
116
117
|
@defaults ||= {}
|
117
118
|
end
|
118
119
|
|
119
|
-
# An accessor to set HTTP request headers for all of the client's
|
120
|
-
# resources.
|
121
|
-
#
|
122
|
-
# hash - An optional Hash of key/value pairs that are sent as HTTP
|
123
|
-
# request headers when a resource's request is performed.
|
124
|
-
#
|
125
|
-
# Returns a Hash of the headers.
|
126
|
-
def headers(hash=nil)
|
127
|
-
@headers = hash unless hash.nil?
|
128
|
-
@headers ||= {}
|
129
|
-
end
|
130
|
-
|
131
|
-
# Send a Rack middleware to all of the requests generated by
|
132
|
-
# the client.
|
133
|
-
#
|
134
|
-
# middleware - An object that implements the rack middleware interface.
|
135
|
-
# args - Zero or more optional arguments to send to the middleware.
|
136
|
-
# block - An optional block to send to the middleware.
|
137
|
-
#
|
138
|
-
# Returns an Array of middlewares.
|
139
|
-
def use(middleware, *args, &block)
|
140
|
-
@middlewares ||= []
|
141
|
-
@middlewares << [middleware, args, block]
|
142
|
-
end
|
143
|
-
|
144
120
|
# Internal: Create and build a resource description of a request. The
|
145
121
|
# resource is then stored in an internal hash, generating an instance
|
146
122
|
# method.
|
@@ -158,10 +134,7 @@ module Weary
|
|
158
134
|
resource.optional *optional
|
159
135
|
resource.required *required
|
160
136
|
resource.defaults defaults
|
161
|
-
resource
|
162
|
-
if !@middlewares.nil? && !@middlewares.empty?
|
163
|
-
@middlewares.each {|middleware| resource.use *middleware }
|
164
|
-
end
|
137
|
+
pass_values_onto_requestable resource
|
165
138
|
yield resource if block_given?
|
166
139
|
self[name] = resource
|
167
140
|
end
|
data/lib/weary/deferred.rb
CHANGED
data/lib/weary/env.rb
CHANGED
@@ -17,7 +17,7 @@ module Weary
|
|
17
17
|
'SERVER_NAME' => @request.uri.host,
|
18
18
|
'SERVER_PORT' => port,
|
19
19
|
'REQUEST_URI' => @request.uri.request_uri,
|
20
|
-
'HTTP_HOST' =>
|
20
|
+
'HTTP_HOST' => http_host,
|
21
21
|
'rack.url_scheme' => @request.uri.scheme,
|
22
22
|
'rack.input' => @request.body,
|
23
23
|
'weary.request' => @request
|
@@ -28,5 +28,10 @@ module Weary
|
|
28
28
|
(@request.uri.port || @request.uri.inferred_port).to_s
|
29
29
|
end
|
30
30
|
|
31
|
+
def http_host
|
32
|
+
uri = @request.uri
|
33
|
+
uri.host + (uri.normalized_port ? ":#{uri.normalized_port}" : "")
|
34
|
+
end
|
35
|
+
|
31
36
|
end
|
32
37
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module Weary
|
4
|
+
module Middleware
|
5
|
+
class Timeout
|
6
|
+
|
7
|
+
def initialize(app, time = 15)
|
8
|
+
@app = app
|
9
|
+
@time = time
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
begin
|
14
|
+
::Timeout.timeout(@time) { @app.call(env) }
|
15
|
+
rescue ::Timeout::Error => e
|
16
|
+
[504, {'Content-Type' => "text/plain"}, [e.message]]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|