koala 1.1.0rc → 1.1.0rc3
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 +1 -0
- data/.travis.yml +8 -0
- data/CHANGELOG +12 -7
- data/koala.gemspec +5 -5
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +78 -117
- data/lib/koala/graph_batch_api.rb +87 -0
- data/lib/koala/graph_collection.rb +54 -0
- data/lib/koala/http_services/net_http_service.rb +31 -26
- data/lib/koala/http_services/typhoeus_service.rb +4 -4
- data/lib/koala/http_services.rb +5 -3
- data/lib/koala/oauth.rb +3 -3
- data/lib/koala/rest_api.rb +1 -1
- data/lib/koala/uploadable_io.rb +122 -90
- data/lib/koala.rb +10 -5
- data/readme.md +7 -7
- data/spec/cases/api_base_spec.rb +2 -2
- data/spec/cases/graph_api_batch_spec.rb +171 -162
- data/spec/cases/http_services/http_service_spec.rb +27 -27
- data/spec/cases/http_services/net_http_service_spec.rb +171 -105
- data/spec/cases/http_services/typhoeus_service_spec.rb +2 -2
- data/spec/cases/oauth_spec.rb +1 -1
- data/spec/cases/realtime_updates_spec.rb +3 -3
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/mock_facebook_responses.yml +33 -22
- data/spec/spec_helper.rb +1 -1
- data/spec/support/graph_api_shared_examples.rb +79 -35
- data/spec/support/mock_http_service.rb +3 -0
- data/spec/support/rest_api_shared_examples.rb +2 -2
- data/spec/support/setup_mocks_or_live.rb +1 -2
- metadata +24 -51
- data/lib/koala/graph_api_batch.rb +0 -151
|
@@ -3,14 +3,14 @@ require "typhoeus" unless defined?(Typhoeus)
|
|
|
3
3
|
module Koala
|
|
4
4
|
module TyphoeusService
|
|
5
5
|
# this service uses Typhoeus to send requests to the graph
|
|
6
|
-
include Typhoeus
|
|
6
|
+
include Typhoeus
|
|
7
7
|
include Koala::HTTPService
|
|
8
8
|
|
|
9
9
|
def self.make_request(path, args, verb, options = {})
|
|
10
10
|
# if the verb isn't get or post, send it as a post argument
|
|
11
11
|
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
|
12
12
|
|
|
13
|
-
# switch any UploadableIOs to the files Typhoeus expects
|
|
13
|
+
# switch any UploadableIOs to the files Typhoeus expects
|
|
14
14
|
args.each_pair {|key, value| args[key] = value.to_file if value.is_a?(UploadableIO)}
|
|
15
15
|
|
|
16
16
|
# you can pass arguments directly to Typhoeus using the :typhoeus_options key
|
|
@@ -27,9 +27,9 @@ module Koala
|
|
|
27
27
|
response = Typhoeus::Request.send(verb, "#{prefix}://#{server(options)}#{path}", typhoeus_options)
|
|
28
28
|
Koala::Response.new(response.code, response.body, response.headers_hash)
|
|
29
29
|
end
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
protected
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def self.multipart_requires_content_type?
|
|
34
34
|
false # Typhoeus handles multipart file types, we don't have to require it
|
|
35
35
|
end
|
data/lib/koala/http_services.rb
CHANGED
|
@@ -13,18 +13,20 @@ module Koala
|
|
|
13
13
|
def self.included(base)
|
|
14
14
|
base.class_eval do
|
|
15
15
|
class << self
|
|
16
|
-
attr_accessor :always_use_ssl, :proxy, :timeout
|
|
16
|
+
attr_accessor :always_use_ssl, :proxy, :timeout
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.server(options = {})
|
|
20
|
-
|
|
20
|
+
server = "#{options[:rest_api] ? Facebook::REST_SERVER : Facebook::GRAPH_SERVER}"
|
|
21
|
+
server.gsub!(/\.facebook/, "-video.facebook") if options[:video]
|
|
22
|
+
"#{options[:beta] ? "beta." : ""}#{server}"
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def self.encode_params(param_hash)
|
|
24
26
|
# unfortunately, we can't use to_query because that's Rails, not Ruby
|
|
25
27
|
# if no hash (e.g. no auth token) return empty string
|
|
26
28
|
((param_hash || {}).collect do |key_and_value|
|
|
27
|
-
key_and_value[1] = key_and_value[1]
|
|
29
|
+
key_and_value[1] = MultiJson.encode(key_and_value[1]) unless key_and_value[1].is_a? String
|
|
28
30
|
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
|
29
31
|
end).join("&")
|
|
30
32
|
end
|
data/lib/koala/oauth.rb
CHANGED
|
@@ -99,7 +99,7 @@ module Koala
|
|
|
99
99
|
def parse_signed_request(input)
|
|
100
100
|
encoded_sig, encoded_envelope = input.split('.', 2)
|
|
101
101
|
signature = base64_url_decode(encoded_sig).unpack("H*").first
|
|
102
|
-
envelope =
|
|
102
|
+
envelope = MultiJson.decode(base64_url_decode(encoded_envelope))
|
|
103
103
|
|
|
104
104
|
raise "SignedRequest: Unsupported algorithm #{envelope['algorithm']}" if envelope['algorithm'] != 'HMAC-SHA256'
|
|
105
105
|
|
|
@@ -126,7 +126,7 @@ module Koala
|
|
|
126
126
|
})
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
MultiJson.decode(response)
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def get_tokens_from_session_keys(sessions, options = {})
|
|
@@ -149,7 +149,7 @@ module Koala
|
|
|
149
149
|
result = fetch_token_string(args, post, "access_token", options)
|
|
150
150
|
|
|
151
151
|
# if we have an error, parse the error JSON and raise an error
|
|
152
|
-
raise APIError.new((
|
|
152
|
+
raise APIError.new((MultiJson.decode(result)["error"] rescue nil) || {}) if result =~ /error/
|
|
153
153
|
|
|
154
154
|
# otherwise, parse the access token
|
|
155
155
|
parse_access_token(result)
|
data/lib/koala/rest_api.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Koala
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def fql_multiquery(queries = {}, args = {}, options = {})
|
|
11
|
-
if results = rest_call('fql.multiquery', args.merge(:queries => queries
|
|
11
|
+
if results = rest_call('fql.multiquery', args.merge(:queries => MultiJson.encode(queries)), options)
|
|
12
12
|
# simplify the multiquery result format
|
|
13
13
|
results.inject({}) {|outcome, data| outcome[data["name"]] = data["fql_result_set"]; outcome}
|
|
14
14
|
end
|
data/lib/koala/uploadable_io.rb
CHANGED
|
@@ -36,108 +36,140 @@ module Koala
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
private
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
DETECTION_STRATEGIES = [
|
|
40
|
+
:sinatra_param?,
|
|
41
|
+
:rails_3_param?,
|
|
42
|
+
:file_param?
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
PARSE_STRATEGIES = [
|
|
46
|
+
:parse_rails_3_param,
|
|
47
|
+
:parse_sinatra_param,
|
|
48
|
+
:parse_file_object,
|
|
49
|
+
:parse_string_path
|
|
50
|
+
]
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
:parse_string_path
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
def parse_init_mixed_param(mixed)
|
|
53
|
-
PARSE_STRATEGIES.each do |method|
|
|
54
|
-
send(method, mixed)
|
|
55
|
-
return if @io_or_path && @content_type
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Expects a parameter of type ActionDispatch::Http::UploadedFile
|
|
60
|
-
def self.rails_3_param?(uploaded_file)
|
|
61
|
-
uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def parse_rails_3_param(uploaded_file)
|
|
65
|
-
if UploadableIO.rails_3_param?(uploaded_file)
|
|
66
|
-
@io_or_path = uploaded_file.tempfile.path
|
|
67
|
-
@content_type = uploaded_file.content_type
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Expects a Sinatra hash of file info
|
|
72
|
-
def self.sinatra_param?(file_hash)
|
|
73
|
-
file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
|
|
52
|
+
def parse_init_mixed_param(mixed)
|
|
53
|
+
PARSE_STRATEGIES.each do |method|
|
|
54
|
+
send(method, mixed)
|
|
55
|
+
return if @io_or_path && @content_type
|
|
74
56
|
end
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Expects a parameter of type ActionDispatch::Http::UploadedFile
|
|
60
|
+
def self.rails_3_param?(uploaded_file)
|
|
61
|
+
uploaded_file.respond_to?(:content_type) and uploaded_file.respond_to?(:tempfile) and uploaded_file.tempfile.respond_to?(:path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parse_rails_3_param(uploaded_file)
|
|
65
|
+
if UploadableIO.rails_3_param?(uploaded_file)
|
|
66
|
+
@io_or_path = uploaded_file.tempfile.path
|
|
67
|
+
@content_type = uploaded_file.content_type
|
|
81
68
|
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Expects a Sinatra hash of file info
|
|
72
|
+
def self.sinatra_param?(file_hash)
|
|
73
|
+
file_hash.kind_of?(Hash) and file_hash.has_key?(:type) and file_hash.has_key?(:tempfile)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def parse_sinatra_param(file_hash)
|
|
77
|
+
if UploadableIO.sinatra_param?(file_hash)
|
|
78
|
+
@io_or_path = file_hash[:tempfile]
|
|
79
|
+
@content_type = file_hash[:type] || detect_mime_type(tempfile)
|
|
86
80
|
end
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# takes a file object
|
|
84
|
+
def self.file_param?(file)
|
|
85
|
+
file.kind_of?(File)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_file_object(file)
|
|
89
|
+
if UploadableIO.file_param?(file)
|
|
90
|
+
@io_or_path = file
|
|
91
|
+
@content_type = detect_mime_type(file.path)
|
|
93
92
|
end
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def parse_string_path(path)
|
|
96
|
+
if path.kind_of?(String)
|
|
97
|
+
@io_or_path = path
|
|
98
|
+
@content_type = detect_mime_type(path)
|
|
100
99
|
end
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
MIME_TYPE_STRATEGIES = [
|
|
103
|
+
:use_mime_module,
|
|
104
|
+
:use_simple_detection
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
def detect_mime_type(filename)
|
|
108
|
+
if filename
|
|
109
|
+
MIME_TYPE_STRATEGIES.each do |method|
|
|
110
|
+
result = send(method, filename)
|
|
111
|
+
return result if result
|
|
113
112
|
end
|
|
114
|
-
nil # if we can't find anything
|
|
115
113
|
end
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
nil # if we can't find anything
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def use_mime_module(filename)
|
|
118
|
+
# if the user has installed mime/types, we can use that
|
|
119
|
+
# if not, rescue and return nil
|
|
120
|
+
begin
|
|
121
|
+
type = MIME::Types.type_for(filename).first
|
|
122
|
+
type ? type.to_s : nil
|
|
123
|
+
rescue
|
|
124
|
+
nil
|
|
126
125
|
end
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def use_simple_detection(filename)
|
|
129
|
+
# very rudimentary extension analysis for images
|
|
130
|
+
# first, get the downcased extension, or an empty string if it doesn't exist
|
|
131
|
+
extension = ((filename.match(/\.([a-zA-Z0-9]+)$/) || [])[1] || "").downcase
|
|
132
|
+
case extension
|
|
133
|
+
when ""
|
|
133
134
|
nil
|
|
134
|
-
|
|
135
|
+
# images
|
|
136
|
+
when "jpg", "jpeg"
|
|
135
137
|
"image/jpeg"
|
|
136
|
-
|
|
138
|
+
when "png"
|
|
137
139
|
"image/png"
|
|
138
|
-
|
|
140
|
+
when "gif"
|
|
139
141
|
"image/gif"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
|
|
143
|
+
# video
|
|
144
|
+
when "3g2"
|
|
145
|
+
"video/3gpp2"
|
|
146
|
+
when "3gp", "3gpp"
|
|
147
|
+
"video/3gpp"
|
|
148
|
+
when "asf"
|
|
149
|
+
"video/x-ms-asf"
|
|
150
|
+
when "avi"
|
|
151
|
+
"video/x-msvideo"
|
|
152
|
+
when "flv"
|
|
153
|
+
"video/x-flv"
|
|
154
|
+
when "m4v"
|
|
155
|
+
"video/x-m4v"
|
|
156
|
+
when "mkv"
|
|
157
|
+
"video/x-matroska"
|
|
158
|
+
when "mod"
|
|
159
|
+
"video/mod"
|
|
160
|
+
when "mov", "qt"
|
|
161
|
+
"video/quicktime"
|
|
162
|
+
when "mp4", "mpeg4"
|
|
163
|
+
"video/mp4"
|
|
164
|
+
when "mpe", "mpeg", "mpg", "tod", "vob"
|
|
165
|
+
"video/mpeg"
|
|
166
|
+
when "nsv"
|
|
167
|
+
"application/x-winamp"
|
|
168
|
+
when "ogm", "ogv"
|
|
169
|
+
"video/ogg"
|
|
170
|
+
when "wmv"
|
|
171
|
+
"video/x-ms-wmv"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
142
174
|
end
|
|
143
175
|
end
|
data/lib/koala.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'digest/md5'
|
|
3
3
|
|
|
4
|
-
require '
|
|
4
|
+
require 'multi_json'
|
|
5
5
|
|
|
6
6
|
# OpenSSL and Base64 are required to support signed_request
|
|
7
7
|
require 'openssl'
|
|
@@ -12,7 +12,9 @@ require 'koala/http_services'
|
|
|
12
12
|
require 'koala/http_services/net_http_service'
|
|
13
13
|
require 'koala/oauth'
|
|
14
14
|
require 'koala/graph_api'
|
|
15
|
-
require 'koala/
|
|
15
|
+
require 'koala/graph_batch_api'
|
|
16
|
+
require 'koala/batch_operation'
|
|
17
|
+
require 'koala/graph_collection'
|
|
16
18
|
require 'koala/rest_api'
|
|
17
19
|
require 'koala/realtime_updates'
|
|
18
20
|
require 'koala/test_users'
|
|
@@ -53,8 +55,8 @@ module Koala
|
|
|
53
55
|
|
|
54
56
|
# parse the body as JSON and run it through the error checker (if provided)
|
|
55
57
|
# Note: Facebook sometimes sends results like "true" and "false", which aren't strictly objects
|
|
56
|
-
# and cause
|
|
57
|
-
body =
|
|
58
|
+
# and cause MultiJson.decode to fail -- so we account for that by wrapping the result in []
|
|
59
|
+
body = MultiJson.decode("[#{result.body.to_s}]")[0]
|
|
58
60
|
yield body if error_checking_block
|
|
59
61
|
|
|
60
62
|
# if we want a component other than the body (e.g. redirect header for images), return that
|
|
@@ -66,9 +68,12 @@ module Koala
|
|
|
66
68
|
|
|
67
69
|
class GraphAPI < API
|
|
68
70
|
include GraphAPIMethods
|
|
69
|
-
include GraphAPIBatchMethods
|
|
70
71
|
end
|
|
71
72
|
|
|
73
|
+
class GraphBatchAPI < GraphAPI
|
|
74
|
+
include GraphBatchAPIMethods
|
|
75
|
+
end
|
|
76
|
+
|
|
72
77
|
class RestAPI < API
|
|
73
78
|
include RestAPIMethods
|
|
74
79
|
end
|
data/readme.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Koala
|
|
2
2
|
====
|
|
3
|
-
Koala
|
|
3
|
+
[Koala](http://github.com/arsduo/koala) is a new Facebook library for Ruby, supporting the Graph API (including the batch requests and photo uploads), the REST API, realtime updates, test users, and OAuth validation. We wrote Koala with four goals:
|
|
4
4
|
|
|
5
|
-
* Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, just over 750 lines of code.)
|
|
6
|
-
* Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course,
|
|
5
|
+
* Lightweight: Koala should be as light and simple as Facebook’s own new libraries, providing API accessors and returning simple JSON. (We clock in, with comments, at just over 750 lines of code.)
|
|
6
|
+
* Fast: Koala should, out of the box, be quick. In addition to supporting the vanilla Ruby networking libraries, it natively supports Typhoeus, our preferred gem for making fast HTTP requests. Of course, that brings us to our next topic:
|
|
7
7
|
* Flexible: Koala should be useful to everyone, regardless of their current configuration. (We have no dependencies beyond the JSON gem. Koala also has a built-in mechanism for using whichever HTTP library you prefer to make requests against the graph.)
|
|
8
8
|
* Tested: Koala should have complete test coverage, so you can rely on it. (Our complete test coverage can be run against either mocked responses or the live Facebook servers.)
|
|
9
9
|
|
|
@@ -47,7 +47,7 @@ When retrieving data that returns an array of results (for example, when calling
|
|
|
47
47
|
# This is useful for paging across multiple requests
|
|
48
48
|
next_path, next_args = feed.next_page_params
|
|
49
49
|
|
|
50
|
-
# You can use those params to easily get the next (or
|
|
50
|
+
# You can use those params to easily get the next (or previous) page
|
|
51
51
|
page = graph.get_page(feed.next_page_params)
|
|
52
52
|
|
|
53
53
|
You can make multiple calls at once using Facebook's batch API:
|
|
@@ -144,12 +144,12 @@ Testing
|
|
|
144
144
|
Unit tests are provided for all of Koala's methods. By default, these tests run against mock responses and hence are ready out of the box:
|
|
145
145
|
|
|
146
146
|
# From anywhere in the project directory:
|
|
147
|
-
rake spec
|
|
147
|
+
bundle exec rake spec
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
You can also run live tests against Facebook's servers:
|
|
151
151
|
|
|
152
152
|
# Again from anywhere in the project directory:
|
|
153
|
-
LIVE=true rake spec
|
|
153
|
+
LIVE=true bundle exec rake spec
|
|
154
154
|
|
|
155
|
-
Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get
|
|
155
|
+
Important Note: to run the live tests, you have to provide some of your own data in spec/fixtures/facebook_data.yml: a valid OAuth access token with publish\_stream, read\_stream, and user\_photos permissions and an OAuth code that can be used to generate an access token. You can get this data at the OAuth Playground; if you want to use your own app, remember to swap out the app ID, secret, and other values. (The file also provides valid values for other tests, which you're welcome to swap out for data specific to your own application.)
|
data/spec/cases/api_base_spec.rb
CHANGED
|
@@ -52,7 +52,7 @@ describe "Koala::Facebook::API" do
|
|
|
52
52
|
Koala.stub(:make_request).and_return(response)
|
|
53
53
|
|
|
54
54
|
json_body = mock('JSON body')
|
|
55
|
-
|
|
55
|
+
MultiJson.stub(:decode).and_return([json_body])
|
|
56
56
|
|
|
57
57
|
@service.api('anything').should == json_body
|
|
58
58
|
end
|
|
@@ -66,7 +66,7 @@ describe "Koala::Facebook::API" do
|
|
|
66
66
|
|
|
67
67
|
@service.api('anything', {}, "get") do |arg|
|
|
68
68
|
yield_test.pass
|
|
69
|
-
arg.should ==
|
|
69
|
+
arg.should == MultiJson.decode(body)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|