api_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +12 -0
  4. data/LICENSE +22 -0
  5. data/README.md +13 -0
  6. data/Rakefile +1 -0
  7. data/api_client.gemspec +24 -0
  8. data/examples/digg.rb +31 -0
  9. data/examples/flickr.rb +37 -0
  10. data/examples/github.rb +52 -0
  11. data/examples/highrise.rb +41 -0
  12. data/examples/twitter.rb +34 -0
  13. data/examples/twitter_oauth.rb +36 -0
  14. data/lib/api_client.rb +45 -0
  15. data/lib/api_client/base.rb +52 -0
  16. data/lib/api_client/connection/abstract.rb +73 -0
  17. data/lib/api_client/connection/basic.rb +105 -0
  18. data/lib/api_client/connection/middlewares/request/logger.rb +17 -0
  19. data/lib/api_client/connection/middlewares/request/oauth.rb +22 -0
  20. data/lib/api_client/connection/oauth.rb +18 -0
  21. data/lib/api_client/errors.rb +16 -0
  22. data/lib/api_client/mixins/configuration.rb +24 -0
  23. data/lib/api_client/mixins/connection_hooks.rb +24 -0
  24. data/lib/api_client/mixins/delegation.rb +23 -0
  25. data/lib/api_client/mixins/inheritance.rb +19 -0
  26. data/lib/api_client/mixins/instantiation.rb +35 -0
  27. data/lib/api_client/mixins/scoping.rb +49 -0
  28. data/lib/api_client/resource/base.rb +63 -0
  29. data/lib/api_client/resource/scope.rb +73 -0
  30. data/lib/api_client/scope.rb +101 -0
  31. data/lib/api_client/utils.rb +18 -0
  32. data/lib/api_client/version.rb +3 -0
  33. data/spec/api_client/base/connection_hook_spec.rb +18 -0
  34. data/spec/api_client/base/delegation_spec.rb +15 -0
  35. data/spec/api_client/base/inheritance_spec.rb +44 -0
  36. data/spec/api_client/base/instantiation_spec.rb +54 -0
  37. data/spec/api_client/base/parsing_spec.rb +36 -0
  38. data/spec/api_client/base/scoping_spec.rb +60 -0
  39. data/spec/api_client/base_spec.rb +17 -0
  40. data/spec/api_client/connection/abstract_spec.rb +21 -0
  41. data/spec/api_client/connection/basic_spec.rb +135 -0
  42. data/spec/api_client/connection/oauth_spec.rb +27 -0
  43. data/spec/api_client/connection/request/logger_spec.rb +19 -0
  44. data/spec/api_client/connection/request/oauth_spec.rb +26 -0
  45. data/spec/api_client/resource/base_spec.rb +78 -0
  46. data/spec/api_client/resource/scope_spec.rb +96 -0
  47. data/spec/api_client/scope_spec.rb +170 -0
  48. data/spec/api_client/utils_spec.rb +32 -0
  49. data/spec/spec_helper.rb +13 -0
  50. data/spec/support/fake_logger.rb +15 -0
  51. data/spec/support/matchers.rb +5 -0
  52. metadata +148 -0
@@ -0,0 +1,101 @@
1
+
2
+ module ApiClient
3
+
4
+ class Scope
5
+ extend ApiClient::Mixins::Configuration
6
+ extend ApiClient::Mixins::Delegation
7
+
8
+ delegate :scoped, :to => :scopeable
9
+
10
+ dsl_accessor :endpoint, :adapter, :return_self => true
11
+
12
+ attr_reader :scopeable
13
+
14
+ def initialize(scopeable)
15
+ @scopeable = scopeable
16
+ @params = {}
17
+ @headers = {}
18
+ @options = {}
19
+ @hooks = @scopeable.connection_hooks || []
20
+ @scopeable.default_scopes.each do |default_scope|
21
+ self.instance_eval(&default_scope)
22
+ end
23
+ end
24
+
25
+ def connection
26
+ klass = Connection.const_get((@adapter || Connection.default).to_s.capitalize)
27
+ @connection = klass.new(@endpoint , @options || {})
28
+ @hooks.each { |hook| hook.call(@connection) }
29
+ @connection
30
+ end
31
+
32
+ # 3 Pillars of scoping
33
+ # options - passed on the the adapter
34
+ # params - converted to query or request body
35
+ # headers - passed on to the request
36
+ def options(new_options = nil)
37
+ return @options if new_options.nil?
38
+ ApiClient::Utils.deep_merge(@options, new_options)
39
+ self
40
+ end
41
+
42
+ def params(options = nil)
43
+ return @params if options.nil?
44
+ ApiClient::Utils.deep_merge(@params, options) if options
45
+ self
46
+ end
47
+ alias :scope :params
48
+
49
+ def headers(options = nil)
50
+ return @headers if options.nil?
51
+ ApiClient::Utils.deep_merge(@headers, options) if options
52
+ self
53
+ end
54
+
55
+ # Half-level :)
56
+ # This is a swiss-army knife kind of method, extremely useful
57
+ def fetch(path, options = {})
58
+ scoped(self) do
59
+ @scopeable.build get(path, options)
60
+ end
61
+ end
62
+
63
+ # Low-level connection methods
64
+
65
+ def request(method, path, options = {})
66
+ raw = options.delete(:raw)
67
+ params(options)
68
+ response = connection.send method, path, @params, @headers
69
+ raw ? response : @scopeable.parse(response)
70
+ end
71
+
72
+ def get(path, options = {})
73
+ request(:get, path, options)
74
+ end
75
+
76
+ def post(path, options = {})
77
+ request(:post, path, options)
78
+ end
79
+
80
+ def put(path, options = {})
81
+ request(:put, path, options)
82
+ end
83
+
84
+ def delete(path, options = {})
85
+ request(:delete, path, options)
86
+ end
87
+
88
+ # Dynamic delegation of scopeable methods
89
+ def method_missing(method, *args, &block)
90
+ if @scopeable.respond_to?(method)
91
+ @scopeable.scoped(self) do
92
+ @scopeable.send(method, *args, &block)
93
+ end
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,18 @@
1
+ module ApiClient
2
+
3
+ module Utils
4
+
5
+ def self.deep_merge(hash, other_hash)
6
+ other_hash.each_pair do |key,v|
7
+ if hash[key].is_a?(::Hash) and v.is_a?(::Hash)
8
+ deep_merge hash[key], v
9
+ else
10
+ hash[key] = v
11
+ end
12
+ end
13
+ hash
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,3 @@
1
+ module ApiClient
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ describe '.connection' do
6
+
7
+ it "registers a new connection_hook" do
8
+ ConnectionHookTestProc = lambda {}
9
+ class ConnectionHookTest < ApiClient::Base
10
+ connection &ConnectionHookTestProc
11
+ end
12
+ ConnectionHookTest.connection_hooks.size.should == 1
13
+ ConnectionHookTest.connection_hooks.should == [ConnectionHookTestProc]
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ it "delegates methods to scope" do
6
+ scope = mock
7
+ ApiClient::Base.stub(:scope).and_return(scope)
8
+ [:fetch, :get, :put, :post, :delete, :headers, :endpoint, :options, :adapter, :params].each do |method|
9
+ scope.should_receive(method)
10
+ ApiClient::Base.send(method)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,44 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ describe "subclasses" do
6
+
7
+ it "inherit scopes, hooks, namespace and format" do
8
+
9
+ class Level1InheritanceTest < ApiClient::Base
10
+ end
11
+
12
+ Level1InheritanceTest.default_scopes.should == []
13
+ Level1InheritanceTest.connection_hooks.should == []
14
+ Level1InheritanceTest.namespace.should == nil
15
+ Level1InheritanceTest.format.should == :json
16
+
17
+ Level1InheritanceTest.default_scopes = ['scope1']
18
+ Level1InheritanceTest.connection_hooks = ['hook1']
19
+ Level1InheritanceTest.namespace 'level1'
20
+ Level1InheritanceTest.format :xml
21
+
22
+ ApiClient::Base.default_scopes.should == []
23
+ ApiClient::Base.connection_hooks.should == []
24
+ ApiClient::Base.namespace.should == nil
25
+ ApiClient::Base.format.should == :json
26
+
27
+ class Level2InheritanceTest < Level1InheritanceTest
28
+ namespace "level2"
29
+ format :yaml
30
+ end
31
+
32
+ Level2InheritanceTest.default_scopes.should == ['scope1']
33
+ Level2InheritanceTest.connection_hooks.should == ['hook1']
34
+ Level2InheritanceTest.namespace.should == 'level2'
35
+ Level2InheritanceTest.format.should == :yaml
36
+
37
+ Level1InheritanceTest.namespace.should == 'level1'
38
+ Level1InheritanceTest.format.should == :xml
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ describe "build" do
6
+
7
+ it "instantiates an array of objects and returns an array if passed an array" do
8
+ result = ApiClient::Base.build [{ :id => 1 }, { :id => 2}]
9
+ result.should be_an_instance_of(Array)
10
+ result.first.should be_an_instance_of(ApiClient::Base)
11
+ result.last.should be_an_instance_of(ApiClient::Base)
12
+ end
13
+
14
+ it "instantiates an object and returns an object if passed an object" do
15
+ result = ApiClient::Base.build({ :id => 1 })
16
+ result.should be_an_instance_of(ApiClient::Base)
17
+ end
18
+
19
+ end
20
+
21
+ describe "build_one" do
22
+
23
+ it "extracts the attributes from a namespace if a namespace is provided" do
24
+ ApiClient::Base.stub(:namespace).and_return("base")
25
+ result = ApiClient::Base.build({ "base" => { :id => 1 } })
26
+ result.should be_an_instance_of(ApiClient::Base)
27
+ result.keys.should == ['id']
28
+ result.id.should == 1
29
+ end
30
+
31
+ end
32
+
33
+ describe "sub hashes" do
34
+
35
+ it "are Hashie::Mashes" do
36
+ result = ApiClient::Base.build({ :id => 1, :subhash => { :foo => 1 } })
37
+ result.subhash.should be_an_instance_of(Hashie::Mash)
38
+ end
39
+
40
+ end
41
+
42
+ describe "original_scope" do
43
+
44
+ it "holds the original scope it was created from" do
45
+
46
+ scope = ApiClient::Base.params(:foo => 1).headers('token' => 'aaa')
47
+ instance = scope.build :key => 'value'
48
+ instance.original_scope.should == scope
49
+
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,36 @@
1
+ require "spec_helper"
2
+
3
+ require "multi_xml"
4
+
5
+ describe ApiClient::Base do
6
+
7
+ it "parses json if json is set as format" do
8
+ ApiClient::Base.stub(:format).and_return(:json)
9
+ parsed = ApiClient::Base.parse('{"a":"1"}')
10
+ parsed.should == {"a"=> "1"}
11
+ end
12
+
13
+ it "parses xml if xml is set as format" do
14
+ ApiClient::Base.stub(:format).and_return(:xml)
15
+ parsed = ApiClient::Base.parse('<a>1</a>')
16
+ parsed.should == {"a"=> "1"}
17
+ end
18
+
19
+ it "returns the string if parser is not found" do
20
+ ApiClient::Base.stub(:format).and_return(:unknown)
21
+ parsed = ApiClient::Base.parse('a:1')
22
+ parsed.should == "a:1"
23
+ end
24
+
25
+ it "extracts the body of a Faraday::Response if it is provided" do
26
+ response = Faraday::Response.new(:body => '{"a": "1"}')
27
+ ApiClient::Base.stub(:format).and_return(:json)
28
+ parsed = ApiClient::Base.parse(response)
29
+ parsed.should == {"a"=> "1"}
30
+
31
+ end
32
+
33
+
34
+
35
+
36
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ describe '.always' do
6
+
7
+ it "registers a new default_scope" do
8
+ AlwaysTestProc = lambda {}
9
+ class AlwaysTest < ApiClient::Base
10
+ always &AlwaysTestProc
11
+ end
12
+ AlwaysTest.default_scopes.size.should == 1
13
+ AlwaysTest.default_scopes.should == [AlwaysTestProc]
14
+ end
15
+
16
+ end
17
+
18
+ describe '.scope' do
19
+
20
+ it "returns a ApiClient::Scope instance" do
21
+ ApiClient::Base.scope.should be_an_instance_of(ApiClient::Scope)
22
+ end
23
+
24
+ end
25
+
26
+ describe '.scope_thread_attribute_name' do
27
+
28
+ it "returns the key under which all .scoped calls should be stored" do
29
+ ApiClient::Base.scope_thread_attribute_name.should == "ApiClient::Base_scope"
30
+ end
31
+
32
+ end
33
+
34
+ describe '.scoped' do
35
+
36
+ it "stores the scope in the thread context, attached to class name" do
37
+ mock_scope3 = mock
38
+ ApiClient::Base.scoped(mock_scope3) do
39
+ Thread.new {
40
+ mock_scope2 = mock
41
+ ApiClient::Base.scoped(mock_scope2) do
42
+ ApiClient::Base.scope.should == mock_scope2
43
+ Thread.current[ApiClient::Base.scope_thread_attribute_name].should == [mock_scope2]
44
+ end
45
+ }
46
+ ApiClient::Base.scope.should == mock_scope3
47
+ Thread.current[ApiClient::Base.scope_thread_attribute_name].should == [mock_scope3]
48
+ end
49
+ Thread.new {
50
+ mock_scope = mock
51
+ ApiClient::Base.scoped(mock_scope) do
52
+ ApiClient::Base.scope.should == mock_scope
53
+ Thread.current[ApiClient::Base.scope_thread_attribute_name].should == [mock_scope]
54
+ end
55
+ }
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,17 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Base do
4
+
5
+ it "is a subclass of Hashie::Mash" do
6
+ ApiClient::Base.should inherit_from(Hashie::Mash)
7
+ end
8
+
9
+ it "responds to #id" do
10
+ subject.should respond_to("id")
11
+ end
12
+
13
+ it "has a nice inspect" do
14
+ subject.update(:id => 1).inspect.should == '#<ApiClient::Base id: 1>'
15
+ end
16
+
17
+ end
@@ -0,0 +1,21 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Connection::Abstract do
4
+
5
+ class ConnectionSubclass < ApiClient::Connection::Abstract
6
+ end
7
+
8
+ it "does not raise an error when instantiating a subclass" do
9
+ lambda {
10
+ ConnectionSubclass.new("http://google.com")
11
+ }.should_not raise_error("Cannot instantiate abstract class")
12
+ end
13
+
14
+ it "raises an error when instantiating directly and not as a subclass" do
15
+ lambda {
16
+ ApiClient::Connection::Abstract.new("http://google.com")
17
+ }.should raise_error("Cannot instantiate abstract class")
18
+ end
19
+
20
+ end
21
+
@@ -0,0 +1,135 @@
1
+ require "spec_helper"
2
+
3
+ describe ApiClient::Connection::Basic do
4
+
5
+ it "has a nice inspect" do
6
+ instance = ApiClient::Connection::Basic.new("http://google.com")
7
+ instance.inspect.should == '#<ApiClient::Connection::Basic endpoint: "http://google.com">'
8
+ end
9
+
10
+ it "adds basic middlewares to faraday" do
11
+ instance = ApiClient::Connection::Basic.new("http://google.com")
12
+ instance.handler.builder.handlers.collect(&:name).should == ["Faraday::Request::UrlEncoded", "Faraday::Adapter::NetHttp"]
13
+ end
14
+
15
+ it "adds the logger middlewares to faraday if ApiClient.logger is available" do
16
+ logger = mock
17
+ ApiClient.stub(:logger).and_return(logger)
18
+ instance = ApiClient::Connection::Basic.new("http://google.com")
19
+ instance.handler.builder.handlers.collect(&:name).should == [
20
+ "ApiClient::Connection::Middlewares::Request::Logger",
21
+ "Faraday::Request::UrlEncoded",
22
+ "Faraday::Adapter::NetHttp"
23
+ ]
24
+
25
+ end
26
+
27
+ it "creates a Faraday object on initialize" do
28
+ instance = ApiClient::Connection::Basic.new("http://google.com")
29
+ instance.handler.should be_an_instance_of(Faraday::Connection)
30
+ end
31
+
32
+ describe "requests" do
33
+
34
+ before do
35
+ @instance = ApiClient::Connection::Basic.new("http://google.com")
36
+ @headers = { "header" => "token" }
37
+ @params = { :param => 1, :nested => { :param => 1 } }
38
+ @response = Faraday::Response.new(:status => 200)
39
+ end
40
+
41
+ it "can perform GET requests" do
42
+ @instance.handler.should_receive(:get).with("/home?param=1&nested[param]=1", @headers).and_return(@response)
43
+ @instance.get "/home", { :param => 1, :nested => { :param => 1 } }, @headers
44
+ end
45
+
46
+ it "can perform POST requests" do
47
+ @instance.handler.should_receive(:post).with("/home", @params, @headers).and_return(@response)
48
+ @instance.post "/home", @params, @headers
49
+ end
50
+
51
+ it "can perform PUT requests" do
52
+ @instance.handler.should_receive(:put).with("/home", @params, @headers).and_return(@response)
53
+ @instance.put "/home", @params, @headers
54
+ end
55
+
56
+ it "can perform DELETE requests" do
57
+ @instance.handler.should_receive(:delete).with("/home?param=1&nested[param]=1", @headers).and_return(@response)
58
+ @instance.delete "/home", @params, @headers
59
+ end
60
+
61
+ end
62
+
63
+ describe "#handle_response" do
64
+
65
+ before do
66
+ @instance = ApiClient::Connection::Basic.new("http://google.com")
67
+ @response = Faraday::Response.new(:status => 200)
68
+ end
69
+
70
+ it "raises an ApiClient::Errors::ConnectionFailed if there is no response" do
71
+ lambda {
72
+ @instance.send :handle_response, nil
73
+ }.should raise_error(ApiClient::Errors::ConnectionFailed)
74
+ end
75
+
76
+ it "raises an ApiClient::Errors::Unauthorized if status is 401" do
77
+ @response.env[:status] = 401
78
+ lambda {
79
+ @instance.send :handle_response, @response
80
+ }.should raise_error(ApiClient::Errors::Unauthorized)
81
+ end
82
+
83
+ it "raises an ApiClient::Errors::Forbidden if status is 403" do
84
+ @response.env[:status] = 403
85
+ lambda {
86
+ @instance.send :handle_response, @response
87
+ }.should raise_error(ApiClient::Errors::Forbidden)
88
+ end
89
+
90
+ it "raises an ApiClient::Errors::NotFound if status is 404" do
91
+ @response.env[:status] = 404
92
+ lambda {
93
+ @instance.send :handle_response, @response
94
+ }.should raise_error(ApiClient::Errors::NotFound)
95
+ end
96
+
97
+ it "raises an ApiClient::Errors::BadRequest if status is 400" do
98
+ @response.env[:status] = 400
99
+ lambda {
100
+ @instance.send :handle_response, @response
101
+ }.should raise_error(ApiClient::Errors::BadRequest)
102
+ end
103
+
104
+ it "raises an ApiClient::Errors::Unsupported if status is 406" do
105
+ @response.env[:status] = 406
106
+ lambda {
107
+ @instance.send :handle_response, @response
108
+ }.should raise_error(ApiClient::Errors::Unsupported)
109
+ end
110
+
111
+ it "raises an ApiClient::Errors::Unsupported if status is 422" do
112
+ @response.env[:status] = 422
113
+ lambda {
114
+ @instance.send :handle_response, @response
115
+ }.should raise_error(ApiClient::Errors::UnprocessableEntity)
116
+ end
117
+
118
+ it "raises an ApiClient::Errors::Unsupported if status is 300..399" do
119
+ @response.env[:status] = 302
120
+ @response.env[:response_headers] = { 'Location' => "https://google.com" }
121
+ lambda {
122
+ @instance.send :handle_response, @response
123
+ }.should raise_error(ApiClient::Errors::Redirect)
124
+ end
125
+
126
+ it "raises an ApiClient::Errors::ServerError if status is 500..599" do
127
+ @response.env[:status] = 502
128
+ lambda {
129
+ @instance.send :handle_response, @response
130
+ }.should raise_error(ApiClient::Errors::ServerError)
131
+ end
132
+
133
+ end
134
+
135
+ end