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