singly 0.1.0 → 0.1.1
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/lib/singly.rb +5 -1
- data/lib/singly/account.rb +4 -4
- data/lib/singly/api/profiles.rb +5 -2
- data/lib/singly/api/profiles/service.rb +6 -3
- data/lib/singly/api/services.rb +4 -2
- data/lib/singly/api/services/37signals.rb +0 -1
- data/lib/singly/api/services/bodymedia.rb +1 -2
- data/lib/singly/api/services/dropbox.rb +1 -2
- data/lib/singly/api/services/dwolla.rb +1 -2
- data/lib/singly/api/services/endpoint.rb +1 -1
- data/lib/singly/api/services/facebook.rb +1 -2
- data/lib/singly/api/services/fitbit.rb +1 -2
- data/lib/singly/api/services/flickr.rb +1 -4
- data/lib/singly/api/services/foursquare.rb +1 -2
- data/lib/singly/api/services/gcal.rb +1 -2
- data/lib/singly/api/services/gcontacts.rb +1 -2
- data/lib/singly/api/services/gdocs.rb +1 -2
- data/lib/singly/api/services/github.rb +1 -2
- data/lib/singly/api/services/gmail.rb +1 -2
- data/lib/singly/api/services/google.rb +1 -2
- data/lib/singly/api/services/gplus.rb +1 -2
- data/lib/singly/api/services/imgur.rb +1 -2
- data/lib/singly/api/services/instagram.rb +1 -2
- data/lib/singly/api/services/klout.rb +1 -2
- data/lib/singly/api/services/linkedin.rb +1 -2
- data/lib/singly/api/services/meetup.rb +1 -2
- data/lib/singly/api/services/paypal.rb +1 -2
- data/lib/singly/api/services/picasa.rb +1 -2
- data/lib/singly/api/services/rdio.rb +1 -2
- data/lib/singly/api/services/reddit.rb +1 -2
- data/lib/singly/api/services/runkeeper.rb +1 -2
- data/lib/singly/api/services/service.rb +9 -31
- data/lib/singly/api/services/shutterfly.rb +1 -2
- data/lib/singly/api/services/soundcloud.rb +1 -2
- data/lib/singly/api/services/stocktwits.rb +1 -2
- data/lib/singly/api/services/tout.rb +1 -2
- data/lib/singly/api/services/tumblr.rb +1 -2
- data/lib/singly/api/services/twitter.rb +1 -2
- data/lib/singly/api/services/withings.rb +1 -2
- data/lib/singly/api/services/wordpress.rb +1 -2
- data/lib/singly/api/services/yammer.rb +1 -2
- data/lib/singly/api/services/youtube.rb +1 -2
- data/lib/singly/api/services/zeo.rb +1 -2
- data/lib/singly/endpoint.rb +52 -43
- data/lib/singly/error.rb +6 -4
- data/lib/singly/http.rb +7 -8
- data/lib/version.rb +1 -1
- data/spec/integration/account/apply_spec.rb +36 -0
- data/spec/integration/account/delete_spec.rb +23 -0
- data/spec/integration/account/friends_spec.rb +51 -0
- data/spec/integration/account/id_spec.rb +23 -0
- data/spec/integration/account/merge_spec.rb +14 -0
- data/spec/integration/account/profile_spec.rb +14 -0
- data/spec/integration/account/profiles_spec.rb +64 -0
- data/spec/integration/account/proxy_spec.rb +43 -0
- data/spec/integration/account/services_spec.rb +62 -0
- data/spec/integration/account/types_spec.rb +57 -0
- data/spec/integration/api_spec.rb +77 -0
- data/spec/singly/account_spec.rb +8 -8
- data/spec/singly/endpoint_spec.rb +53 -45
- data/spec/singly/error_spec.rb +21 -13
- data/spec/singly/http_spec.rb +26 -13
- data/spec/singly_spec.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/vcr_cassettes/{http_fetch_with_api_error.yml → singly/http_fetch_with_api_error.yml} +3 -3
- data/spec/vcr_cassettes/{http_fetch_with_json_response.yml → singly/http_fetch_with_json_response.yml} +2 -2
- data/spec/vcr_cassettes/{http_fetch_with_non_json_response.yml → singly/http_fetch_with_non_json_response.yml} +2 -2
- metadata +30 -12
- data/spec/integration/auth_spec.rb +0 -14
- data/spec/singly/api/auth_spec.rb +0 -8
data/lib/singly/endpoint.rb
CHANGED
@@ -8,71 +8,86 @@
|
|
8
8
|
#
|
9
9
|
module Singly
|
10
10
|
module Endpoint
|
11
|
-
attr_reader :
|
12
|
-
attr_reader :
|
13
|
-
attr_reader :route_params
|
14
|
-
attr_reader :query_params
|
11
|
+
attr_reader :path
|
12
|
+
attr_reader :options
|
15
13
|
|
16
14
|
def initialize(params={})
|
17
|
-
params
|
18
|
-
|
19
|
-
@type = self.class.type
|
20
|
-
route_param_keys = self.class.required_route_params
|
21
|
-
@query_params = params.reject{|key| route_param_keys.include? key }
|
22
|
-
@route_params = params.select{|key| route_param_keys.include? key }
|
15
|
+
init_path(params)
|
16
|
+
init_options(params)
|
23
17
|
end
|
24
18
|
|
25
|
-
|
19
|
+
# The opts argument is for Typhoeus overrides
|
20
|
+
def fetch(opts={})
|
26
21
|
validate
|
27
|
-
Singly::Http.fetch(
|
22
|
+
Singly::Http.fetch(path, options.merge(opts))
|
28
23
|
end
|
29
24
|
|
30
25
|
# Raises an error if any required parameters have not been
|
31
26
|
# supplied or if any path components are missing.
|
32
27
|
def validate
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
Singly::Error.y_u_no?("has query params :#{missing_query_params.join(', :')}") { missing_query_params.any? }
|
28
|
+
Singly::Error.y_u_no?("has all route params: #{path}") { path.include? ":" }
|
29
|
+
missing_options = self.class.required_params - options[:params].keys
|
30
|
+
Singly::Error.y_u_no?("has required params :#{missing_options.join(', :')}") { missing_options.any? }
|
37
31
|
true
|
38
32
|
end
|
39
33
|
|
40
|
-
# Constructs the path by replacing path components
|
41
|
-
# defined in the route with their corresponding
|
42
|
-
# values in the @parameters attribute.
|
43
|
-
# "/profiles/:id@:service" -> "/profiles/12345@twitter"
|
44
|
-
def path
|
45
|
-
route_params.inject(route) do |route, param|
|
46
|
-
route.sub(":#{param[0]}", CGI.escape("#{param[1]}"))
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
34
|
# String representation of the composed endpoint.
|
51
35
|
# Parameter order is deterministic so it can be
|
52
36
|
# used as a key in the /multi endpoint.
|
53
37
|
def url
|
54
38
|
validate
|
55
|
-
query_string =
|
39
|
+
query_string = options[:params].sort.inject([]) do |queries, param|
|
56
40
|
queries << ("#{CGI.escape(param[0].to_s)}=#{CGI.escape(param[1].to_s)}")
|
57
41
|
end.join("&")
|
58
42
|
url = "#{Singly::Http.base_url}#{path}"
|
59
43
|
url += "?#{query_string}" unless query_string.empty?
|
60
|
-
|
44
|
+
url
|
61
45
|
end
|
62
46
|
|
63
47
|
private
|
64
48
|
|
49
|
+
# Constructs the path by replacing route components
|
50
|
+
# with their corresponding values specified in the
|
51
|
+
# params argument.
|
52
|
+
# "/profiles/:id@:service" -> "/profiles/12345@twitter"
|
53
|
+
def init_path(params)
|
54
|
+
route = self.class.route
|
55
|
+
route_keys = route.scan(/:(\w+)/).flatten
|
56
|
+
@path = route_keys.inject(route) do |path, key|
|
57
|
+
value = params.delete(key.to_sym)
|
58
|
+
value ? path.sub(":#{key}", CGI.escape("#{value}")) : path
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def init_options(params)
|
63
|
+
@options = {
|
64
|
+
method: self.class.http_method,
|
65
|
+
timeout: Singly.timeout,
|
66
|
+
params: {}
|
67
|
+
}
|
68
|
+
if [:post, :put].include? @options[:method]
|
69
|
+
# Always send credentials in the url
|
70
|
+
@options[:params][:access_token] = params.delete(:access_token) if params[:access_token]
|
71
|
+
@options[:params][:client_id] = params.delete(:client_id) if params[:client_id]
|
72
|
+
@options[:params][:client_id] = params.delete(:client_secret) if params[:client_secret]
|
73
|
+
@options[:body] = params
|
74
|
+
else
|
75
|
+
@options[:params] = params
|
76
|
+
end
|
77
|
+
@options
|
78
|
+
end
|
79
|
+
|
65
80
|
# Use this to pass credentials down the pipe
|
66
81
|
# eg:
|
67
82
|
# def photos(params={})
|
68
83
|
# Singly::Services::Facebook.new(creds.merge(params))
|
69
84
|
# end
|
70
85
|
def creds
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
86
|
+
[:access_token, :client_id, :client_secret].inject({}) do |creds, cred_key|
|
87
|
+
cred = options[:params][cred_key]
|
88
|
+
creds[cred_key] = cred if cred
|
89
|
+
creds
|
90
|
+
end
|
76
91
|
end
|
77
92
|
|
78
93
|
def self.included(base)
|
@@ -80,18 +95,14 @@ module Singly
|
|
80
95
|
end
|
81
96
|
|
82
97
|
module ClassMethods
|
83
|
-
def
|
84
|
-
@
|
98
|
+
def http_method
|
99
|
+
@http_method || :get
|
85
100
|
end
|
86
101
|
|
87
102
|
def route
|
88
103
|
@route || ""
|
89
104
|
end
|
90
105
|
|
91
|
-
def required_route_params
|
92
|
-
route.scan(/:(\w+)/).flatten.map(&:to_sym)
|
93
|
-
end
|
94
|
-
|
95
106
|
def required_params
|
96
107
|
(@params && @params[:required]) || []
|
97
108
|
end
|
@@ -100,10 +111,8 @@ module Singly
|
|
100
111
|
(@params && @params[:optional]) || []
|
101
112
|
end
|
102
113
|
|
103
|
-
|
104
|
-
|
105
|
-
def endpoint(type, route, params={})
|
106
|
-
@type = type
|
114
|
+
def endpoint(http_method, route, params={})
|
115
|
+
@http_method = http_method
|
107
116
|
@route = route
|
108
117
|
@params = params
|
109
118
|
end
|
data/lib/singly/error.rb
CHANGED
@@ -12,13 +12,15 @@ module Singly
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
class ApiError < Error
|
15
|
-
|
16
|
-
|
15
|
+
attr_reader :response
|
16
|
+
def initialize(response, msg=nil)
|
17
|
+
@response = response
|
18
|
+
super(msg || "HTTP #{response.code} #{response.body}")
|
17
19
|
end
|
18
20
|
end
|
19
|
-
class TimeoutError <
|
21
|
+
class TimeoutError < ApiError
|
20
22
|
def initialize(response)
|
21
|
-
super("
|
23
|
+
super(response, "Timed out after #{response.time} seconds")
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
data/lib/singly/http.rb
CHANGED
@@ -7,22 +7,21 @@ module Singly
|
|
7
7
|
"https://api.singly.com/#{Singly.version}"
|
8
8
|
end
|
9
9
|
|
10
|
-
def fetch(
|
11
|
-
|
12
|
-
|
13
|
-
log("PARAMS #{params}")
|
14
|
-
response = Typhoeus::Request.send(type, path, params: params, timeout: Singly.timeout)
|
10
|
+
def fetch(path, options={})
|
11
|
+
log("#{path} with #{options}")
|
12
|
+
response = Typhoeus::Request.new("#{base_url}#{path}", options).run
|
15
13
|
validate_response(response)
|
16
14
|
parse_response(response)
|
17
15
|
end
|
18
16
|
|
19
17
|
private
|
20
18
|
|
21
|
-
#
|
22
|
-
#
|
19
|
+
# Naive response handling.
|
20
|
+
# Singly sometimes returns invalid json with the application/json
|
21
|
+
# content type. For example, a simple "true" is received from
|
22
|
+
# the POST /profiles/self endpoint.
|
23
23
|
def parse_response(response)
|
24
24
|
body = response.body
|
25
|
-
log(body)
|
26
25
|
JSON.parse(body) rescue body
|
27
26
|
end
|
28
27
|
|
data/lib/version.rb
CHANGED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Singly.account(:access_token).merge" do
|
4
|
+
let(:account) { Singly.account("TOKEN_STUB", "ACCOUNT_ID_STUB") }
|
5
|
+
before { Singly.client_id = "CLIENT_ID_STUB" }
|
6
|
+
before { Singly.client_secret = "CLIENT_SECRET_STUB" }
|
7
|
+
describe "GET /auth/:service/apply with Oauth1 service" do
|
8
|
+
subject { account.apply(:flickr, token: "a_token", token_secret: "a_secret") }
|
9
|
+
it { subject.validate.should == true }
|
10
|
+
it { subject.path.should == "/auth/flickr/apply" }
|
11
|
+
it { subject.options.should include(
|
12
|
+
method: :get,
|
13
|
+
params: {
|
14
|
+
token: "a_token",
|
15
|
+
token_secret: "a_secret",
|
16
|
+
account: "ACCOUNT_ID_STUB",
|
17
|
+
client_id: "CLIENT_ID_STUB",
|
18
|
+
client_secret: "CLIENT_SECRET_STUB"
|
19
|
+
}
|
20
|
+
)}
|
21
|
+
end
|
22
|
+
describe "GET /auth/:service/apply with Oauth1 service" do
|
23
|
+
subject { account.apply(:facebook, token: "a_token") }
|
24
|
+
it { subject.validate.should == true }
|
25
|
+
it { subject.path.should == "/auth/facebook/apply" }
|
26
|
+
it { subject.options.should include(
|
27
|
+
method: :get,
|
28
|
+
params: {
|
29
|
+
token: "a_token",
|
30
|
+
account: "ACCOUNT_ID_STUB",
|
31
|
+
client_id: "CLIENT_ID_STUB",
|
32
|
+
client_secret: "CLIENT_SECRET_STUB"
|
33
|
+
}
|
34
|
+
)}
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Singly.account(:access_token).delete" do
|
4
|
+
let(:account) { Singly.account("TOKEN_STUB") }
|
5
|
+
describe "DELETE /profiles/:service" do
|
6
|
+
subject { account.delete(:instagram) }
|
7
|
+
it { subject.validate.should == true }
|
8
|
+
it { subject.path.should == "/profiles/instagram" }
|
9
|
+
it { subject.options.should include(
|
10
|
+
method: :delete,
|
11
|
+
params: { access_token: "TOKEN_STUB" }
|
12
|
+
)}
|
13
|
+
end
|
14
|
+
describe "DELETE /profiles/:id@:service" do
|
15
|
+
subject { account.delete(:foursquare, "123abc") }
|
16
|
+
it { subject.validate.should == true }
|
17
|
+
it { subject.path.should == "/profiles/123abc@foursquare" }
|
18
|
+
it { subject.options.should include(
|
19
|
+
method: :delete,
|
20
|
+
params: { access_token: "TOKEN_STUB" }
|
21
|
+
)}
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Singly.account(:access_token).friends" do
|
4
|
+
let(:account) { Singly.account("TOKEN_STUB") }
|
5
|
+
describe "GET /friends" do
|
6
|
+
subject { account.friends }
|
7
|
+
it { subject.validate.should == true }
|
8
|
+
it { subject.path.should == "/friends" }
|
9
|
+
it { subject.options.should include(
|
10
|
+
method: :get,
|
11
|
+
params: { access_token: "TOKEN_STUB" }
|
12
|
+
)}
|
13
|
+
end
|
14
|
+
describe "GET /friends/:group" do
|
15
|
+
subject { account.friends.peers }
|
16
|
+
it { subject.validate.should == true }
|
17
|
+
it { subject.path.should == "/friends/peers" }
|
18
|
+
it { subject.options.should include(
|
19
|
+
method: :get,
|
20
|
+
params: { access_token: "TOKEN_STUB" }
|
21
|
+
)}
|
22
|
+
end
|
23
|
+
describe "GET /friends/:group with optional params" do
|
24
|
+
subject { account.friends.all({
|
25
|
+
limit: 10,
|
26
|
+
offset: 3,
|
27
|
+
services: "facebook,linkedin",
|
28
|
+
full: true,
|
29
|
+
sort: "interactions",
|
30
|
+
toc: false,
|
31
|
+
q: "kevrone",
|
32
|
+
bio: "Timehop"
|
33
|
+
}) }
|
34
|
+
it { subject.validate.should == true }
|
35
|
+
it { subject.path.should == "/friends/all" }
|
36
|
+
it { subject.options.should include(
|
37
|
+
method: :get,
|
38
|
+
params: {
|
39
|
+
access_token: "TOKEN_STUB",
|
40
|
+
limit: 10,
|
41
|
+
offset: 3,
|
42
|
+
services: "facebook,linkedin",
|
43
|
+
full: true,
|
44
|
+
sort: "interactions",
|
45
|
+
toc: false,
|
46
|
+
q: "kevrone",
|
47
|
+
bio: "Timehop"
|
48
|
+
}
|
49
|
+
)}
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Singly.account(:access_token).id" do
|
4
|
+
let(:account) { Singly.account("TOKEN_STUB") }
|
5
|
+
describe "GET /id/:id" do
|
6
|
+
subject { account.id("photo:123456@facebook/photos#987654321") }
|
7
|
+
it { subject.validate.should == true }
|
8
|
+
it { subject.path.should == "/id/photo%3A123456%40facebook%2Fphotos%23987654321" }
|
9
|
+
it { subject.options.should include(
|
10
|
+
method: :get,
|
11
|
+
params: { access_token: "TOKEN_STUB" }
|
12
|
+
)}
|
13
|
+
end
|
14
|
+
describe "GET /id/:id with optional params" do
|
15
|
+
subject { account.id("photo:123456@facebook/photos#987654321", map: true) }
|
16
|
+
it { subject.validate.should == true }
|
17
|
+
it { subject.path.should == "/id/photo%3A123456%40facebook%2Fphotos%23987654321" }
|
18
|
+
it { subject.options.should include(
|
19
|
+
method: :get,
|
20
|
+
params: { access_token: "TOKEN_STUB", map: true }
|
21
|
+
)}
|
22
|
+
end
|
23
|
+
end
|