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.
Files changed (56) hide show
  1. data/.gitignore +4 -1
  2. data/.rspec +2 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +11 -8
  5. data/Gemfile.lock +49 -53
  6. data/LICENSE +1 -1
  7. data/README.md +134 -208
  8. data/Rakefile +6 -47
  9. data/lib/weary.rb +4 -66
  10. data/lib/weary/adapter.rb +23 -0
  11. data/lib/weary/adapters/net_http.rb +68 -0
  12. data/lib/weary/client.rb +243 -0
  13. data/lib/weary/deferred.rb +35 -0
  14. data/lib/weary/env.rb +32 -0
  15. data/lib/weary/middleware.rb +9 -0
  16. data/lib/weary/middleware/basic_auth.rb +17 -0
  17. data/lib/weary/middleware/content_type.rb +28 -0
  18. data/lib/weary/middleware/oauth.rb +31 -0
  19. data/lib/weary/request.rb +137 -124
  20. data/lib/weary/resource.rb +152 -128
  21. data/lib/weary/response.rb +48 -99
  22. data/lib/weary/route.rb +53 -0
  23. data/lib/weary/version.rb +3 -0
  24. data/spec/spec_helper.rb +4 -56
  25. data/spec/support/shared_examples_for_a_rack_app.rb +70 -0
  26. data/spec/support/shared_examples_for_a_rack_env.rb +14 -0
  27. data/spec/support/shared_examples_for_a_uri.rb +9 -0
  28. data/spec/weary/adapter_spec.rb +26 -0
  29. data/spec/weary/adapters/nethttp_spec.rb +88 -0
  30. data/spec/weary/client_spec.rb +258 -0
  31. data/spec/weary/deferred_spec.rb +35 -0
  32. data/spec/weary/env_spec.rb +12 -0
  33. data/spec/weary/middleware/basic_auth_spec.rb +23 -0
  34. data/spec/weary/middleware/content_type_spec.rb +34 -0
  35. data/spec/weary/middleware/oauth_spec.rb +27 -0
  36. data/spec/weary/request_spec.rb +227 -315
  37. data/spec/weary/resource_spec.rb +233 -233
  38. data/spec/weary/response_spec.rb +82 -159
  39. data/spec/weary/route_spec.rb +72 -0
  40. data/spec/weary_spec.rb +3 -56
  41. data/weary.gemspec +16 -79
  42. metadata +138 -98
  43. data/VERSION +0 -1
  44. data/examples/batch.rb +0 -20
  45. data/examples/repo.rb +0 -16
  46. data/examples/status.rb +0 -26
  47. data/lib/weary/base.rb +0 -124
  48. data/lib/weary/batch.rb +0 -37
  49. data/lib/weary/exceptions.rb +0 -15
  50. data/lib/weary/httpverb.rb +0 -32
  51. data/spec/fixtures/github.yml +0 -11
  52. data/spec/fixtures/twitter.xml +0 -763
  53. data/spec/fixtures/vimeo.json +0 -1
  54. data/spec/weary/base_spec.rb +0 -320
  55. data/spec/weary/batch_spec.rb +0 -71
  56. data/spec/weary/httpverb_spec.rb +0 -25
@@ -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
- attr_reader :raw, :requester, :code, :message, :header, :content_type, :cookie, :body, :url
5
-
6
- def initialize(http_response, requester)
7
- @raw = http_response
8
- @requester = requester
9
- @url = requester.uri
10
- @code = http_response.code.to_i
11
- @message = http_response.message
12
- @header = http_response.to_hash
13
- @content_type = http_response.content_type
14
- @cookie = http_response['Set-Cookie']
15
- @body = http_response.body
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
- raw.is_a? Net::HTTPRedirection
21
- end
22
-
23
- # Was this Request successful?
24
- def success?
25
- (200..299).include?(code)
42
+ @response.redirection?
26
43
  end
27
-
28
- # Returns a symbol corresponding to the Response's Content Type
29
- def format
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
- # Follow the Redirect
45
- def follow_redirect
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 StandardError, "The Response has no body. #{requester.via.to_s.upcase} request sent." unless body
56
- handle_errors
57
- case format
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Weary
2
+ VERSION = "1.0.0.rc1"
3
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,58 +1,6 @@
1
- begin
2
- # Try to require the preresolved locked set of gems.
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
- begin
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
- require 'spec'
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