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.
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/Gemfile +12 -0
- data/LICENSE +22 -0
- data/README.md +13 -0
- data/Rakefile +1 -0
- data/api_client.gemspec +24 -0
- data/examples/digg.rb +31 -0
- data/examples/flickr.rb +37 -0
- data/examples/github.rb +52 -0
- data/examples/highrise.rb +41 -0
- data/examples/twitter.rb +34 -0
- data/examples/twitter_oauth.rb +36 -0
- data/lib/api_client.rb +45 -0
- data/lib/api_client/base.rb +52 -0
- data/lib/api_client/connection/abstract.rb +73 -0
- data/lib/api_client/connection/basic.rb +105 -0
- data/lib/api_client/connection/middlewares/request/logger.rb +17 -0
- data/lib/api_client/connection/middlewares/request/oauth.rb +22 -0
- data/lib/api_client/connection/oauth.rb +18 -0
- data/lib/api_client/errors.rb +16 -0
- data/lib/api_client/mixins/configuration.rb +24 -0
- data/lib/api_client/mixins/connection_hooks.rb +24 -0
- data/lib/api_client/mixins/delegation.rb +23 -0
- data/lib/api_client/mixins/inheritance.rb +19 -0
- data/lib/api_client/mixins/instantiation.rb +35 -0
- data/lib/api_client/mixins/scoping.rb +49 -0
- data/lib/api_client/resource/base.rb +63 -0
- data/lib/api_client/resource/scope.rb +73 -0
- data/lib/api_client/scope.rb +101 -0
- data/lib/api_client/utils.rb +18 -0
- data/lib/api_client/version.rb +3 -0
- data/spec/api_client/base/connection_hook_spec.rb +18 -0
- data/spec/api_client/base/delegation_spec.rb +15 -0
- data/spec/api_client/base/inheritance_spec.rb +44 -0
- data/spec/api_client/base/instantiation_spec.rb +54 -0
- data/spec/api_client/base/parsing_spec.rb +36 -0
- data/spec/api_client/base/scoping_spec.rb +60 -0
- data/spec/api_client/base_spec.rb +17 -0
- data/spec/api_client/connection/abstract_spec.rb +21 -0
- data/spec/api_client/connection/basic_spec.rb +135 -0
- data/spec/api_client/connection/oauth_spec.rb +27 -0
- data/spec/api_client/connection/request/logger_spec.rb +19 -0
- data/spec/api_client/connection/request/oauth_spec.rb +26 -0
- data/spec/api_client/resource/base_spec.rb +78 -0
- data/spec/api_client/resource/scope_spec.rb +96 -0
- data/spec/api_client/scope_spec.rb +170 -0
- data/spec/api_client/utils_spec.rb +32 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/fake_logger.rb +15 -0
- data/spec/support/matchers.rb +5 -0
- 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
|