weary 0.7.2 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|