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.
Files changed (70) hide show
  1. data/lib/singly.rb +5 -1
  2. data/lib/singly/account.rb +4 -4
  3. data/lib/singly/api/profiles.rb +5 -2
  4. data/lib/singly/api/profiles/service.rb +6 -3
  5. data/lib/singly/api/services.rb +4 -2
  6. data/lib/singly/api/services/37signals.rb +0 -1
  7. data/lib/singly/api/services/bodymedia.rb +1 -2
  8. data/lib/singly/api/services/dropbox.rb +1 -2
  9. data/lib/singly/api/services/dwolla.rb +1 -2
  10. data/lib/singly/api/services/endpoint.rb +1 -1
  11. data/lib/singly/api/services/facebook.rb +1 -2
  12. data/lib/singly/api/services/fitbit.rb +1 -2
  13. data/lib/singly/api/services/flickr.rb +1 -4
  14. data/lib/singly/api/services/foursquare.rb +1 -2
  15. data/lib/singly/api/services/gcal.rb +1 -2
  16. data/lib/singly/api/services/gcontacts.rb +1 -2
  17. data/lib/singly/api/services/gdocs.rb +1 -2
  18. data/lib/singly/api/services/github.rb +1 -2
  19. data/lib/singly/api/services/gmail.rb +1 -2
  20. data/lib/singly/api/services/google.rb +1 -2
  21. data/lib/singly/api/services/gplus.rb +1 -2
  22. data/lib/singly/api/services/imgur.rb +1 -2
  23. data/lib/singly/api/services/instagram.rb +1 -2
  24. data/lib/singly/api/services/klout.rb +1 -2
  25. data/lib/singly/api/services/linkedin.rb +1 -2
  26. data/lib/singly/api/services/meetup.rb +1 -2
  27. data/lib/singly/api/services/paypal.rb +1 -2
  28. data/lib/singly/api/services/picasa.rb +1 -2
  29. data/lib/singly/api/services/rdio.rb +1 -2
  30. data/lib/singly/api/services/reddit.rb +1 -2
  31. data/lib/singly/api/services/runkeeper.rb +1 -2
  32. data/lib/singly/api/services/service.rb +9 -31
  33. data/lib/singly/api/services/shutterfly.rb +1 -2
  34. data/lib/singly/api/services/soundcloud.rb +1 -2
  35. data/lib/singly/api/services/stocktwits.rb +1 -2
  36. data/lib/singly/api/services/tout.rb +1 -2
  37. data/lib/singly/api/services/tumblr.rb +1 -2
  38. data/lib/singly/api/services/twitter.rb +1 -2
  39. data/lib/singly/api/services/withings.rb +1 -2
  40. data/lib/singly/api/services/wordpress.rb +1 -2
  41. data/lib/singly/api/services/yammer.rb +1 -2
  42. data/lib/singly/api/services/youtube.rb +1 -2
  43. data/lib/singly/api/services/zeo.rb +1 -2
  44. data/lib/singly/endpoint.rb +52 -43
  45. data/lib/singly/error.rb +6 -4
  46. data/lib/singly/http.rb +7 -8
  47. data/lib/version.rb +1 -1
  48. data/spec/integration/account/apply_spec.rb +36 -0
  49. data/spec/integration/account/delete_spec.rb +23 -0
  50. data/spec/integration/account/friends_spec.rb +51 -0
  51. data/spec/integration/account/id_spec.rb +23 -0
  52. data/spec/integration/account/merge_spec.rb +14 -0
  53. data/spec/integration/account/profile_spec.rb +14 -0
  54. data/spec/integration/account/profiles_spec.rb +64 -0
  55. data/spec/integration/account/proxy_spec.rb +43 -0
  56. data/spec/integration/account/services_spec.rb +62 -0
  57. data/spec/integration/account/types_spec.rb +57 -0
  58. data/spec/integration/api_spec.rb +77 -0
  59. data/spec/singly/account_spec.rb +8 -8
  60. data/spec/singly/endpoint_spec.rb +53 -45
  61. data/spec/singly/error_spec.rb +21 -13
  62. data/spec/singly/http_spec.rb +26 -13
  63. data/spec/singly_spec.rb +1 -1
  64. data/spec/spec_helper.rb +6 -0
  65. data/spec/vcr_cassettes/{http_fetch_with_api_error.yml → singly/http_fetch_with_api_error.yml} +3 -3
  66. data/spec/vcr_cassettes/{http_fetch_with_json_response.yml → singly/http_fetch_with_json_response.yml} +2 -2
  67. data/spec/vcr_cassettes/{http_fetch_with_non_json_response.yml → singly/http_fetch_with_non_json_response.yml} +2 -2
  68. metadata +30 -12
  69. data/spec/integration/auth_spec.rb +0 -14
  70. data/spec/singly/api/auth_spec.rb +0 -8
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :soundcloud
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :stocktwits
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :tout
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :tumblr
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :twitter
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :withings
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :wordpress
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :yammer
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :youtube
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -4,8 +4,7 @@ module Singly
4
4
  include Singly::Endpoint
5
5
  include Singly::Services::Service
6
6
 
7
- service :zeo
8
-
7
+
9
8
  def self(params={})
10
9
  service_endpoint(__method__, params)
11
10
  end
@@ -8,71 +8,86 @@
8
8
  #
9
9
  module Singly
10
10
  module Endpoint
11
- attr_reader :type
12
- attr_reader :route
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 = params || {}
18
- @route = self.class.route
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
- def fetch
19
+ # The opts argument is for Typhoeus overrides
20
+ def fetch(opts={})
26
21
  validate
27
- Singly::Http.fetch(type, path, query_params)
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
- missing_route_params = self.class.required_route_params - route_params.keys
34
- Singly::Error.y_u_no?("has route params :#{missing_route_params.join(', :')}") { missing_route_params.any? }
35
- missing_query_params = self.class.required_params - query_params.keys
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 = query_params.sort.inject([]) do |queries, param|
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
- return url
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
- creds = {}
72
- creds[:access_token] = query_params[:access_token] if query_params[:access_token]
73
- creds[:client_id] = query_params[:client_id] if query_params[:client_id]
74
- creds[:client_secret] = query_params[:client_secret] if query_params[:client_secret]
75
- creds
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 type
84
- @type || :get
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
- private
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
@@ -12,13 +12,15 @@ module Singly
12
12
  end
13
13
  end
14
14
  class ApiError < Error
15
- def initialize(response)
16
- super("#{response.code} #{response.body} #{response.effective_url}")
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 < Error
21
+ class TimeoutError < ApiError
20
22
  def initialize(response)
21
- super("Response timed out after #{response.time} seconds. #{response.effective_url}")
23
+ super(response, "Timed out after #{response.time} seconds")
22
24
  end
23
25
  end
24
26
  end
@@ -7,22 +7,21 @@ module Singly
7
7
  "https://api.singly.com/#{Singly.version}"
8
8
  end
9
9
 
10
- def fetch(type, path, params={})
11
- path = "#{base_url}#{path}"
12
- log("#{type.upcase} #{path}")
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
- # Singly does not yet guarantee that all responses conform to
22
- # valid json. Sometimes we can get a TypeError
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
 
@@ -1,3 +1,3 @@
1
1
  module Singly
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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