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,105 @@
1
+ # Faraday for making requests
2
+ require 'faraday'
3
+
4
+ module ApiClient
5
+
6
+ module Connection
7
+
8
+ class Basic < Abstract
9
+
10
+ def create_handler
11
+ # Create and memoize the connection object
12
+ @handler = Faraday.new(@endpoint, @options[:faraday] || {})
13
+ finalize_handler
14
+ end
15
+
16
+ def finalize_handler
17
+ @handler.use Middlewares::Request::Logger, ApiClient.logger if ApiClient.logger
18
+ @handler.use Faraday::Request::UrlEncoded
19
+ @handler.adapter Faraday.default_adapter
20
+ end
21
+
22
+ #### ApiClient::Connection::Abstract#get
23
+ # Performs a GET request
24
+ # Accepts three parameters:
25
+ #
26
+ # * path - the path the request should go to
27
+ # * data - (optional) the query, passed as a hash and converted into query params
28
+ # * headers - (optional) headers sent along with the request
29
+ #
30
+ def get(path, data = {}, headers = {})
31
+ query = Faraday::Utils.build_nested_query(data || {})
32
+ path = [path, query].join('?') unless query.empty?
33
+ handle_response @handler.get(path, headers)
34
+ end
35
+
36
+ #### ApiClient::Connection::Abstract#post
37
+ # Performs a POST request
38
+ # Accepts three parameters:
39
+ #
40
+ # * path - the path request should go to
41
+ # * data - (optional) data sent in the request
42
+ # * headers - (optional) headers sent along in the request
43
+ #
44
+ # This method automatically adds the application token header
45
+ def post(path, data = {}, headers = {})
46
+ handle_response @handler.post(path, data, headers)
47
+ end
48
+
49
+ #### ApiClient::Connection::Abstract#put
50
+ # Performs a PUT request
51
+ # Accepts three parameters:
52
+ #
53
+ # * path - the path request should go to
54
+ # * data - (optional) data sent in the request
55
+ # * headers - (optional) headers sent along in the request
56
+ #
57
+ # This method automatically adds the application token header
58
+ def put(path, data = {}, headers = {})
59
+ handle_response @handler.put(path, data, headers)
60
+ end
61
+
62
+ #### FS::Connection#delete
63
+ # Performs a DELETE request
64
+ # Accepts three parameters:
65
+ #
66
+ # * path - the path request should go to
67
+ # * data - (optional) the query, passed as a hash and converted into query params
68
+ # * headers - (optional) headers sent along in the request
69
+ #
70
+ # This method automatically adds the application token header
71
+ def delete(path, data = {}, headers = {})
72
+ query = Faraday::Utils.build_nested_query(data || {})
73
+ path = [path, query].join('?') unless query.empty?
74
+ handle_response @handler.delete(path, headers)
75
+ end
76
+
77
+ private
78
+
79
+ def handle_response(response)
80
+ raise ApiClient::Errors::ConnectionFailed if !response
81
+ case response.status
82
+ when 401
83
+ raise ApiClient::Errors::Unauthorized
84
+ when 403
85
+ raise ApiClient::Errors::Forbidden
86
+ when 404
87
+ raise ApiClient::Errors::NotFound
88
+ when 400
89
+ raise ApiClient::Errors::BadRequest
90
+ when 406
91
+ raise ApiClient::Errors::Unsupported
92
+ when 422
93
+ raise ApiClient::Errors::UnprocessableEntity.new(response.body)
94
+ when 300..399
95
+ raise ApiClient::Errors::Redirect.new(response['Location'])
96
+ when 500..599
97
+ raise ApiClient::Errors::ServerError
98
+ else
99
+ response
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,17 @@
1
+ require "logger"
2
+ class ApiClient::Connection::Middlewares::Request::Logger < Faraday::Middleware
3
+
4
+ def call(env)
5
+ time = Time.now
6
+ returns = @app.call(env)
7
+ taken = Time.now - time
8
+ @logger.info "#{env[:method].upcase} #{env[:url]}: #{"%.4f" % taken} seconds"
9
+ returns
10
+ end
11
+
12
+ def initialize(app, logger = nil)
13
+ @logger = logger || ::Logger.new(STDOUT)
14
+ @app = app
15
+ end
16
+
17
+ end
@@ -0,0 +1,22 @@
1
+ # Borrowed from https://github.com/pengwynn/faraday_middleware/blob/master/lib/faraday/request/oauth.rb
2
+ class ApiClient::Connection::Middlewares::Request::OAuth < Faraday::Middleware
3
+
4
+ dependency 'simple_oauth'
5
+
6
+ def call(env)
7
+ params = env[:body] || {}
8
+ signature_params = params.reject{ |k,v| v.respond_to?(:content_type) }
9
+
10
+ header = SimpleOAuth::Header.new(env[:method], env[:url], signature_params, @options || {})
11
+
12
+ env[:request_headers]['Authorization'] = header.to_s
13
+ env[:request_headers]['User-Agent'] = "ApiClient gem v#{ApiClient::VERSION}"
14
+
15
+ @app.call(env)
16
+ end
17
+
18
+ def initialize(app, options = {})
19
+ @app, @options = app, options
20
+ end
21
+
22
+ end
@@ -0,0 +1,18 @@
1
+ module ApiClient
2
+
3
+ module Connection
4
+
5
+ class Oauth < Basic
6
+
7
+ def finalize_handler
8
+ @handler.use Middlewares::Request::Logger, ApiClient.logger if ApiClient.logger
9
+ @handler.use Middlewares::Request::OAuth, @options[:oauth]
10
+ @handler.use Faraday::Request::UrlEncoded
11
+ @handler.adapter Faraday.default_adapter
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,16 @@
1
+ module ApiClient
2
+
3
+ module Errors
4
+ class ConnectionFailed < Exception; end
5
+ class Config < Exception; end
6
+ class Unauthorized < Exception; end
7
+ class Forbidden < Exception; end
8
+ class NotFound < Exception; end
9
+ class Redirect < Exception; end
10
+ class BadRequest < Exception; end
11
+ class Unsupported < Exception; end
12
+ class ServerError < Exception; end
13
+ class UnprocessableEntity < Exception; end
14
+ end
15
+
16
+ end
@@ -0,0 +1,24 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module Configuration
6
+
7
+ def dsl_accessor(*names)
8
+ options = names.last.is_a?(Hash) ? names.pop : {}
9
+ names.each do |name|
10
+ returns = options[:return_self] ? "self" : "@#{name}"
11
+ class_eval <<-STR
12
+ def #{name}(value = nil)
13
+ value.nil? ? @#{name} : @#{name} = value
14
+ #{returns}
15
+ end
16
+ STR
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,24 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module ConnectionHooks
6
+
7
+ attr_accessor :connection_hooks
8
+
9
+ def connection(&block)
10
+ @connection_hooks ||= []
11
+ @connection_hooks.push(block) if block
12
+ @connection_hooks
13
+ end
14
+
15
+ def connection_hooks
16
+ @connection_hooks || []
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
@@ -0,0 +1,23 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module Delegation
6
+
7
+ def delegate(*methods)
8
+ hash = methods.pop
9
+ to = hash[:to]
10
+ methods.each do |method|
11
+ class_eval <<-STR
12
+ def #{method}(*args, &block)
13
+ #{to}.#{method}(*args, &block)
14
+ end
15
+ STR
16
+ end
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+
23
+ end
@@ -0,0 +1,19 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module Inheritance
6
+
7
+ def inherited(subclass)
8
+ subclass.default_scopes = self.default_scopes.dup
9
+ subclass.connection_hooks = self.connection_hooks.dup
10
+
11
+ subclass.namespace self.namespace
12
+ subclass.format self.format
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,35 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module Instantiation
6
+
7
+ def self.extended(base)
8
+ base.instance_eval do
9
+ attr_accessor :original_scope
10
+ end
11
+ end
12
+
13
+ def build_one(hash)
14
+ instance = self.new self.namespace ? hash[namespace] : hash
15
+ instance.original_scope = self.scope
16
+ instance
17
+ end
18
+
19
+ def build_many(array)
20
+ array.collect { |one| build_one(one) }
21
+ end
22
+
23
+ def build(result_or_array)
24
+ if result_or_array.is_a?(Array)
25
+ build_many result_or_array
26
+ else
27
+ build_one result_or_array
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,49 @@
1
+ module ApiClient
2
+
3
+ module Mixins
4
+
5
+ module Scoping
6
+
7
+ attr_accessor :default_scopes
8
+
9
+ # Default scoping
10
+ def always(&block)
11
+ default_scopes.push(block) if block
12
+ end
13
+
14
+ def default_scopes
15
+ @default_scopes || []
16
+ end
17
+
18
+ # Scoping
19
+ def scope(options = {})
20
+ scope_in_thread || Scope.new(self).params(options)
21
+ end
22
+
23
+ # Allow wrapping singleton methods in a scope
24
+ # Store the handler in a thread-local variable for thread safety
25
+ def scoped(scope)
26
+ Thread.current[scope_thread_attribute_name] ||= []
27
+ Thread.current[scope_thread_attribute_name].push scope
28
+ begin
29
+ yield
30
+ ensure
31
+ Thread.current[scope_thread_attribute_name] = nil
32
+ end
33
+ end
34
+
35
+ def scope_thread_attribute_name
36
+ "#{self.name}_scope"
37
+ end
38
+
39
+ def scope_in_thread
40
+ if found = Thread.current[scope_thread_attribute_name]
41
+ found.last
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,63 @@
1
+ module ApiClient
2
+
3
+ module Resource
4
+
5
+ class Base < ApiClient::Base
6
+
7
+ class << self
8
+ extend ApiClient::Mixins::Delegation
9
+ extend ApiClient::Mixins::Configuration
10
+
11
+ delegate :find_all, :find, :create, :update, :destroy, :path, :to => :scope
12
+
13
+ dsl_accessor :prefix
14
+
15
+ def inherited(subclass)
16
+ super
17
+ small_name = subclass.name.split('::').last.downcase
18
+ subclass.namespace small_name
19
+ subclass.prefix self.prefix
20
+ subclass.always do
21
+ name = subclass.name.split('::').last.downcase
22
+ pre_fix = prefix
23
+ path ["", prefix, "#{name}s"].compact.join('/')
24
+ end
25
+ end
26
+
27
+ def scope(options = {})
28
+ scope_in_thread || ApiClient::Resource::Scope.new(self).params(options)
29
+ end
30
+
31
+ end
32
+
33
+ def persisted?
34
+ !!self.id
35
+ end
36
+
37
+ def save
38
+ self.persisted? ? remote_update : remote_create
39
+ end
40
+
41
+ def destroy
42
+ self.class.destroy(self.id)
43
+ end
44
+
45
+ def payload
46
+ hash = self.to_hash
47
+ hash.delete('id') # This key is never required
48
+ hash
49
+ end
50
+
51
+ def remote_update
52
+ self.class.update(self.id, payload)
53
+ end
54
+
55
+ def remote_create
56
+ self.class.create(payload)
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,73 @@
1
+ # This class includes methods for calling restful APIs
2
+ module ApiClient
3
+
4
+ module Resource
5
+
6
+ class Scope < ApiClient::Scope
7
+
8
+ dsl_accessor :path, :return_self => true
9
+
10
+ def format
11
+ @scopeable.format
12
+ end
13
+
14
+ def append_format(path)
15
+ format ? [path, format].join('.') : path
16
+ end
17
+
18
+ def find(id)
19
+ path = [@path, id].join('/')
20
+ path = append_format(path)
21
+ raw = get(path)
22
+ scoped(self) do
23
+ @scopeable.build(raw)
24
+ end
25
+ end
26
+
27
+ def find_all(params = {})
28
+ path = append_format(@path)
29
+ raw = get(path, params)
30
+ scoped(self) do
31
+ @scopeable.build(raw)
32
+ end
33
+ end
34
+
35
+ def create(params = {})
36
+ path = append_format(@path)
37
+ hash = if @scopeable.namespace
38
+ { @scopeable.namespace => params }
39
+ else
40
+ params
41
+ end
42
+ response = post(path, hash)
43
+ scoped(self) do
44
+ @scopeable.build(response)
45
+ end
46
+ end
47
+
48
+ def update(id, params = {})
49
+ path = [@path, id].join('/')
50
+ path = append_format(path)
51
+ hash = if @scopeable.namespace
52
+ { @scopeable.namespace => params }
53
+ else
54
+ params
55
+ end
56
+ response = put(path, hash)
57
+ scoped(self) do
58
+ @scopeable.build(response)
59
+ end
60
+ end
61
+
62
+ def destroy(id)
63
+ path = [@path, id].join('/')
64
+ path = append_format(path)
65
+ delete(path)
66
+ true
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end