weary 0.7.2 → 1.0.0.rc1
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/.gitignore +4 -1
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +11 -8
- data/Gemfile.lock +49 -53
- data/LICENSE +1 -1
- data/README.md +134 -208
- data/Rakefile +6 -47
- data/lib/weary.rb +4 -66
- data/lib/weary/adapter.rb +23 -0
- data/lib/weary/adapters/net_http.rb +68 -0
- data/lib/weary/client.rb +243 -0
- data/lib/weary/deferred.rb +35 -0
- data/lib/weary/env.rb +32 -0
- data/lib/weary/middleware.rb +9 -0
- data/lib/weary/middleware/basic_auth.rb +17 -0
- data/lib/weary/middleware/content_type.rb +28 -0
- data/lib/weary/middleware/oauth.rb +31 -0
- data/lib/weary/request.rb +137 -124
- data/lib/weary/resource.rb +152 -128
- data/lib/weary/response.rb +48 -99
- data/lib/weary/route.rb +53 -0
- data/lib/weary/version.rb +3 -0
- data/spec/spec_helper.rb +4 -56
- data/spec/support/shared_examples_for_a_rack_app.rb +70 -0
- data/spec/support/shared_examples_for_a_rack_env.rb +14 -0
- data/spec/support/shared_examples_for_a_uri.rb +9 -0
- data/spec/weary/adapter_spec.rb +26 -0
- data/spec/weary/adapters/nethttp_spec.rb +88 -0
- data/spec/weary/client_spec.rb +258 -0
- data/spec/weary/deferred_spec.rb +35 -0
- data/spec/weary/env_spec.rb +12 -0
- data/spec/weary/middleware/basic_auth_spec.rb +23 -0
- data/spec/weary/middleware/content_type_spec.rb +34 -0
- data/spec/weary/middleware/oauth_spec.rb +27 -0
- data/spec/weary/request_spec.rb +227 -315
- data/spec/weary/resource_spec.rb +233 -233
- data/spec/weary/response_spec.rb +82 -159
- data/spec/weary/route_spec.rb +72 -0
- data/spec/weary_spec.rb +3 -56
- data/weary.gemspec +16 -79
- metadata +138 -98
- data/VERSION +0 -1
- data/examples/batch.rb +0 -20
- data/examples/repo.rb +0 -16
- data/examples/status.rb +0 -26
- data/lib/weary/base.rb +0 -124
- data/lib/weary/batch.rb +0 -37
- data/lib/weary/exceptions.rb +0 -15
- data/lib/weary/httpverb.rb +0 -32
- data/spec/fixtures/github.yml +0 -11
- data/spec/fixtures/twitter.xml +0 -763
- data/spec/fixtures/vimeo.json +0 -1
- data/spec/weary/base_spec.rb +0 -320
- data/spec/weary/batch_spec.rb +0 -71
- data/spec/weary/httpverb_spec.rb +0 -25
data/lib/weary/response.rb
CHANGED
@@ -1,110 +1,59 @@
|
|
1
|
+
require 'rack/response'
|
2
|
+
|
3
|
+
autoload :MultiJson, 'multi_json'
|
4
|
+
|
1
5
|
module Weary
|
2
6
|
class Response
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@
|
7
|
+
include Rack::Response::Helpers
|
8
|
+
|
9
|
+
def initialize(body, status, headers)
|
10
|
+
@response = Rack::Response.new body, status, headers
|
11
|
+
@status = self.status
|
12
|
+
end
|
13
|
+
|
14
|
+
def status
|
15
|
+
@response.status.to_i
|
16
|
+
end
|
17
|
+
|
18
|
+
def header
|
19
|
+
@response.header
|
20
|
+
end
|
21
|
+
alias headers header
|
22
|
+
|
23
|
+
def body
|
24
|
+
buffer = ""
|
25
|
+
@response.body.each {|chunk| buffer << chunk }
|
26
|
+
buffer
|
27
|
+
end
|
28
|
+
|
29
|
+
def each(&iterator)
|
30
|
+
@response.body.each(&iterator)
|
31
|
+
end
|
32
|
+
|
33
|
+
def finish
|
34
|
+
[status, header, self]
|
35
|
+
end
|
36
|
+
|
37
|
+
def success?
|
38
|
+
@response.successful?
|
16
39
|
end
|
17
|
-
|
18
|
-
# Is this an HTTP redirect?
|
40
|
+
|
19
41
|
def redirected?
|
20
|
-
|
21
|
-
end
|
22
|
-
|
23
|
-
# Was this Request successful?
|
24
|
-
def success?
|
25
|
-
(200..299).include?(code)
|
42
|
+
@response.redirection?
|
26
43
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@format ||= case content_type
|
31
|
-
when *ContentTypes[:json]
|
32
|
-
:json
|
33
|
-
when *ContentTypes[:xml]
|
34
|
-
:xml
|
35
|
-
when *ContentTypes[:html]
|
36
|
-
:html
|
37
|
-
when *ContentTypes[:yaml]
|
38
|
-
:yaml
|
39
|
-
else
|
40
|
-
:plain
|
41
|
-
end
|
44
|
+
|
45
|
+
def length
|
46
|
+
@response.length
|
42
47
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
if redirected?
|
47
|
-
new_request = requester.dup
|
48
|
-
new_request.uri = @raw['location']
|
49
|
-
new_request.perform
|
50
|
-
end
|
48
|
+
|
49
|
+
def call(env)
|
50
|
+
self.finish
|
51
51
|
end
|
52
|
-
|
53
|
-
# Parse the body with Crack parsers (if XML/HTML) or Yaml parser
|
52
|
+
|
54
53
|
def parse
|
55
|
-
raise
|
56
|
-
|
57
|
-
|
58
|
-
when :xml, :html
|
59
|
-
Crack::XML.parse body
|
60
|
-
when :json
|
61
|
-
Crack::JSON.parse body
|
62
|
-
when :yaml
|
63
|
-
YAML::load body
|
64
|
-
else
|
65
|
-
body
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Puts the body
|
70
|
-
def to_s
|
71
|
-
body.to_s
|
72
|
-
end
|
73
|
-
|
74
|
-
# Same as parse[key]
|
75
|
-
def [](key)
|
76
|
-
parse[key]
|
54
|
+
raise "The response does not contain a body" if body.nil? || body.empty?
|
55
|
+
raise "Unable to parse Content-Type: #{content_type}" unless content_type =~ /json($|;.*)/
|
56
|
+
MultiJson.decode body
|
77
57
|
end
|
78
|
-
|
79
|
-
private
|
80
|
-
def handle_errors
|
81
|
-
case @code
|
82
|
-
when 301,302
|
83
|
-
raise RedirectionError, "#{@message} to #{@raw['location']}"
|
84
|
-
when 200...400
|
85
|
-
return
|
86
|
-
when 400
|
87
|
-
raise BadRequest, "Failed with #{@code}: #{@message}"
|
88
|
-
when 401
|
89
|
-
raise Unauthorized, "Failed with #{@code}: #{@message}"
|
90
|
-
when 403
|
91
|
-
raise Forbidden, "Failed with #{@code}: #{@message}"
|
92
|
-
when 404
|
93
|
-
raise NotFound, "Failed with #{@code}: #{@message}"
|
94
|
-
when 405
|
95
|
-
raise MethodNotAllowed, "Failed with #{@code}: #{@message}"
|
96
|
-
when 409
|
97
|
-
raise ResourceConflict, "Failed with #{@code}: #{@message}"
|
98
|
-
when 422
|
99
|
-
raise UnprocessableEntity, "Failed with #{@code}: #{@message}"
|
100
|
-
when 401...500
|
101
|
-
raise ClientError, "Failed with #{@code}: #{@message}"
|
102
|
-
when 500...600
|
103
|
-
raise ServerError, "Failed with #{@code}: #{@message}"
|
104
|
-
else
|
105
|
-
raise HTTPError, "Unknown response code: #{@code}"
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
58
|
end
|
110
59
|
end
|
data/lib/weary/route.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Weary
|
4
|
+
class Route
|
5
|
+
NotFoundError = Class.new(StandardError)
|
6
|
+
NotAllowedError = Class.new(StandardError)
|
7
|
+
|
8
|
+
def initialize(resources, domain="")
|
9
|
+
@resources = resources
|
10
|
+
@domain = domain
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
begin
|
15
|
+
request = Rack::Request.new(env)
|
16
|
+
resource = route(request)
|
17
|
+
url_variables = resource.url.extract("#{@domain}#{request.path}")
|
18
|
+
resource.request(url_variables.merge(request.params)).call(env)
|
19
|
+
rescue NotFoundError => e
|
20
|
+
[404, {'Content-Type' => "text/plain"}, [e.message]]
|
21
|
+
rescue NotAllowedError => e
|
22
|
+
[405, {'Content-Type' => "text/plain"}, [e.message]]
|
23
|
+
rescue Weary::Resource::UnmetRequirementsError => e
|
24
|
+
[403, {'Content-Type' => "text/plain"}, [e.message]]
|
25
|
+
rescue Exception => e
|
26
|
+
[500, {'Content-Type' => "text/plain"}, [e.message]]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def route(request)
|
31
|
+
subset = select_by_url("#{@domain}#{request.path}")
|
32
|
+
raise NotFoundError, "Not Found" if subset.empty?
|
33
|
+
subset = select_by_method(request.request_method, subset)
|
34
|
+
raise NotAllowedError, "Method Not Allowed" if subset.empty?
|
35
|
+
subset.first
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def select_by_url(url, set=@resources)
|
41
|
+
set.select do |resource|
|
42
|
+
!resource.url.extract(url).nil?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def select_by_method(method, set=@resources)
|
47
|
+
set.select do |resource|
|
48
|
+
resource.method.to_s.upcase == method
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,58 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require File.expand_path('../.bundle/environment', __FILE__)
|
4
|
-
rescue LoadError
|
5
|
-
# Fall back on doing an unlocked resolve at runtime.
|
6
|
-
require "rubygems"
|
7
|
-
require "bundler"
|
8
|
-
Bundler.setup
|
9
|
-
end
|
1
|
+
require 'weary'
|
2
|
+
require 'webmock/rspec'
|
10
3
|
|
11
|
-
|
12
|
-
require 'weary'
|
13
|
-
rescue LoadError
|
14
|
-
lib_path = File.join(File.dirname(__FILE__), '..', 'lib')
|
15
|
-
$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
|
16
|
-
require 'weary'
|
17
|
-
end
|
4
|
+
Dir['./spec/support/**/*.rb'].each {|f| require f }
|
18
5
|
|
19
|
-
|
20
|
-
require 'fakeweb'
|
21
|
-
|
22
|
-
def get_fixture(filename)
|
23
|
-
open(File.join(File.dirname(__FILE__), 'fixtures', "#{filename.to_s}")).read
|
24
|
-
end
|
25
|
-
|
26
|
-
def http_status_message(code)
|
27
|
-
message = case code.to_i
|
28
|
-
when 200
|
29
|
-
"OK"
|
30
|
-
when 301
|
31
|
-
"Moved Permanently"
|
32
|
-
when 302
|
33
|
-
"Moved Temporarily"
|
34
|
-
when 400
|
35
|
-
"Bad Request"
|
36
|
-
when 401
|
37
|
-
"Unauthorized"
|
38
|
-
when 403
|
39
|
-
"Forbidden"
|
40
|
-
when 404
|
41
|
-
"Not Found"
|
42
|
-
when 405
|
43
|
-
"Method Not Allowed"
|
44
|
-
when 409
|
45
|
-
"Conflict"
|
46
|
-
when 418
|
47
|
-
"I'm a teapot"
|
48
|
-
when 422
|
49
|
-
"Unprocessable Entity"
|
50
|
-
when 401...500
|
51
|
-
"Client Error"
|
52
|
-
when 500...600
|
53
|
-
"Server Error"
|
54
|
-
else
|
55
|
-
"Unknown"
|
56
|
-
end
|
57
|
-
[code.to_s,message]
|
58
|
-
end
|
6
|
+
WebMock.disable_net_connect!
|
@@ -0,0 +1,70 @@
|
|
1
|
+
shared_examples_for "a Rack application" do
|
2
|
+
it { should respond_to :call }
|
3
|
+
|
4
|
+
it "takes one argument, the environment" do
|
5
|
+
method = if subject.is_a? Module
|
6
|
+
subject.method :call
|
7
|
+
else
|
8
|
+
subject.class.instance_method :call
|
9
|
+
end
|
10
|
+
method.arity.should eq 1
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns an Array" do
|
14
|
+
subject.call(env).should be_an Array
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns with three arguments" do
|
18
|
+
subject.call(env).length.should be 3
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "the status" do
|
22
|
+
let(:status) { subject.call(env).first }
|
23
|
+
|
24
|
+
it "is greater than or equal to 100 when parsed as an integer" do
|
25
|
+
status.to_i.should be >= 100
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "the headers" do
|
30
|
+
let(:headers) { subject.call(env)[1] }
|
31
|
+
|
32
|
+
it { headers.should respond_to :each }
|
33
|
+
|
34
|
+
it "yields values of key and value" do
|
35
|
+
headers.should be_all {|key, value| !key.nil? && !value.nil? }
|
36
|
+
end
|
37
|
+
|
38
|
+
it "contains keys of Strings" do
|
39
|
+
headers.keys.should be_all {|key| key.kind_of? String }
|
40
|
+
end
|
41
|
+
|
42
|
+
it "does not contain a 'Status' key" do
|
43
|
+
headers.keys.map(&:downcase).should_not include 'status'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "contains values of Strings" do
|
47
|
+
headers.values.should be_all {|value| value.kind_of? String }
|
48
|
+
end
|
49
|
+
|
50
|
+
it "contains a 'Content-Type' header" do
|
51
|
+
headers.should have_key 'Content-Type'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "the body" do
|
56
|
+
let(:body) { subject.call(env).last }
|
57
|
+
|
58
|
+
it { body.should respond_to :each }
|
59
|
+
|
60
|
+
it "yields only String values" do
|
61
|
+
map = []
|
62
|
+
body.each {|string| map << string }
|
63
|
+
# We do this strange mapping, since the only requirement
|
64
|
+
# of the body is it responds to :each.
|
65
|
+
# This allows us to use Array predicates
|
66
|
+
map.should be_all {|i| i.kind_of? String }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
shared_examples_for "a Rack env" do
|
2
|
+
# From the Rack spec:
|
3
|
+
# http://rack.rubyforge.org/doc/SPEC.html
|
4
|
+
it { should be_an_instance_of Hash }
|
5
|
+
it { should have_key "REQUEST_METHOD"}
|
6
|
+
it "does not have an empty value for REQUEST_METHOD" do
|
7
|
+
subject["REQUEST_METHOD"].should_not be_empty
|
8
|
+
end
|
9
|
+
it { should have_key "SCRIPT_NAME"}
|
10
|
+
it { should have_key "PATH_INFO"}
|
11
|
+
it { should have_key "QUERY_STRING"}
|
12
|
+
it { should have_key "SERVER_NAME"}
|
13
|
+
it { should have_key "SERVER_PORT"}
|
14
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
shared_examples_for "a URI" do
|
2
|
+
it { uri.should respond_to(:path) }
|
3
|
+
it { uri.should respond_to(:query) }
|
4
|
+
it { uri.should respond_to(:host) }
|
5
|
+
it { uri.should respond_to(:port) }
|
6
|
+
it { uri.should respond_to(:inferred_port) }
|
7
|
+
it { uri.should respond_to(:request_uri) }
|
8
|
+
it { uri.should respond_to(:scheme) }
|
9
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Weary::Adapter do
|
4
|
+
subject { Class.new { include Weary::Adapter }.new }
|
5
|
+
let(:env) {
|
6
|
+
{
|
7
|
+
"REQUEST_METHOD"=>"GET",
|
8
|
+
"SCRIPT_NAME"=>"",
|
9
|
+
"PATH_INFO"=>"/api/v2/json/repos/show/mwunsch/weary",
|
10
|
+
"QUERY_STRING"=>nil,
|
11
|
+
"SERVER_NAME"=>"github.com",
|
12
|
+
"SERVER_PORT"=>80,
|
13
|
+
"REQUEST_URI"=>"/api/v2/json/repos/show/mwunsch/weary"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
|
17
|
+
describe "#call" do
|
18
|
+
it_behaves_like "a Rack application"
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "connect" do
|
22
|
+
it "returns a Rack::Response" do
|
23
|
+
subject.connect(Rack::Request.new(env)).should be_a Rack::Response
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Weary::Adapter::NetHttp do
|
4
|
+
before do
|
5
|
+
@url = "http://github.com/api/v2/json/repos/show/mwunsch/weary"
|
6
|
+
@request = Weary::Request.new @url
|
7
|
+
end
|
8
|
+
|
9
|
+
describe ".call" do
|
10
|
+
before do
|
11
|
+
stub_request(:get, @url).
|
12
|
+
to_return(:status => 200, :body => "", :headers => {})
|
13
|
+
end
|
14
|
+
|
15
|
+
it_behaves_like "a Rack application" do
|
16
|
+
subject { described_class }
|
17
|
+
let(:env) { @request.env }
|
18
|
+
end
|
19
|
+
|
20
|
+
it "performs the request through the connect method" do
|
21
|
+
described_class.stub(:connect) { Rack::Response.new("", 200, {})}
|
22
|
+
described_class.should_receive :connect
|
23
|
+
described_class.call(@request.env)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#call" do
|
28
|
+
it "calls the class method `.call`" do
|
29
|
+
described_class.stub(:call) { [200, {'Content-Type' => 'text/plain'}, [""]] }
|
30
|
+
described_class.should_receive(:call)
|
31
|
+
described_class.new.call(@request.env)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe ".connect" do
|
36
|
+
before do
|
37
|
+
stub_request(:get, @url)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "performs the http request" do
|
41
|
+
req = Rack::Request.new(@request.env)
|
42
|
+
described_class.connect(req)
|
43
|
+
a_request(:get, @url).should have_been_made
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns a Rack::Response" do
|
47
|
+
req = Rack::Request.new(@request.env)
|
48
|
+
described_class.connect(req).should be_kind_of Rack::Response
|
49
|
+
end
|
50
|
+
|
51
|
+
it "sets appropriate request headers" do
|
52
|
+
@request.headers 'User-Agent' => Weary::USER_AGENTS['Lynx 2.8.4rel.1 on Linux']
|
53
|
+
req = Rack::Request.new(@request.env)
|
54
|
+
described_class.connect(req)
|
55
|
+
a_request(:get, @url).with(:headers => @request.headers).should have_been_made
|
56
|
+
end
|
57
|
+
|
58
|
+
it "sets the body of the request" do
|
59
|
+
stub_request(:post, @url)
|
60
|
+
@request.method = "POST"
|
61
|
+
@request.params :foo => "baz"
|
62
|
+
req = Rack::Request.new(@request.env)
|
63
|
+
described_class.connect(req)
|
64
|
+
a_request(:post, @url).with(:body => "foo=baz").should have_been_made
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe ".normalize_request_headers" do
|
69
|
+
it "removes the HTTP_ prefix from request headers" do
|
70
|
+
@request.headers 'User-Agent' => Weary::USER_AGENTS['Lynx 2.8.4rel.1 on Linux']
|
71
|
+
headers = described_class.normalize_request_headers(@request.env)
|
72
|
+
headers.should have_key "User-Agent"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ".socket" do
|
77
|
+
it "sets up the http connection" do
|
78
|
+
req = Rack::Request.new(@request.env)
|
79
|
+
described_class.socket(req).should be_kind_of Net::HTTP
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe ".request_class" do
|
84
|
+
it "gets the Net::HTTP request class for the request method" do
|
85
|
+
described_class.request_class("POST").should be Net::HTTP::Post
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|