api_client 0.1.0

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 (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