koala 1.0.0 → 1.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 (39) hide show
  1. data/.autotest +12 -0
  2. data/.gitignore +3 -1
  3. data/.travis.yml +8 -0
  4. data/CHANGELOG +26 -2
  5. data/Gemfile +4 -0
  6. data/autotest/discover.rb +1 -0
  7. data/koala.gemspec +8 -8
  8. data/lib/koala/batch_operation.rb +74 -0
  9. data/lib/koala/graph_api.rb +103 -102
  10. data/lib/koala/graph_batch_api.rb +87 -0
  11. data/lib/koala/graph_collection.rb +54 -0
  12. data/lib/koala/http_services/net_http_service.rb +92 -0
  13. data/lib/koala/http_services/typhoeus_service.rb +37 -0
  14. data/lib/koala/http_services.rb +13 -113
  15. data/lib/koala/oauth.rb +181 -0
  16. data/lib/koala/realtime_updates.rb +5 -14
  17. data/lib/koala/rest_api.rb +13 -8
  18. data/lib/koala/uploadable_io.rb +137 -77
  19. data/lib/koala.rb +36 -196
  20. data/readme.md +51 -32
  21. data/spec/cases/api_base_spec.rb +4 -4
  22. data/spec/cases/graph_api_batch_spec.rb +609 -0
  23. data/spec/cases/http_services/http_service_spec.rb +87 -12
  24. data/spec/cases/http_services/net_http_service_spec.rb +259 -77
  25. data/spec/cases/http_services/typhoeus_service_spec.rb +29 -21
  26. data/spec/cases/koala_spec.rb +55 -0
  27. data/spec/cases/oauth_spec.rb +1 -1
  28. data/spec/cases/realtime_updates_spec.rb +3 -3
  29. data/spec/cases/test_users_spec.rb +1 -1
  30. data/spec/cases/uploadable_io_spec.rb +56 -14
  31. data/spec/fixtures/cat.m4v +0 -0
  32. data/spec/fixtures/mock_facebook_responses.yml +100 -5
  33. data/spec/spec_helper.rb +2 -1
  34. data/spec/support/graph_api_shared_examples.rb +106 -35
  35. data/spec/support/json_testing_fix.rb +18 -0
  36. data/spec/support/mock_http_service.rb +57 -56
  37. data/spec/support/rest_api_shared_examples.rb +131 -7
  38. data/spec/support/setup_mocks_or_live.rb +3 -4
  39. metadata +34 -47
data/.autotest ADDED
@@ -0,0 +1,12 @@
1
+ # Override autotest default magic to rerun all tests every time a
2
+ # change is detected on the file system.
3
+ class Autotest
4
+
5
+ def get_to_green
6
+ begin
7
+ rerun_all_tests
8
+ wait_for_changes unless all_good
9
+ end until all_good
10
+ end
11
+
12
+ end
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
1
  pkg
2
2
  .project
3
- Gemfile.lock
3
+ Gemfile.lock
4
+ .rvmrc
5
+ *.rbc
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7 # (current default)
3
+ - 1.9.2
4
+ - rbx
5
+ - rbx-2.0
6
+ - ree
7
+ - jruby
8
+ - ruby-head
data/CHANGELOG CHANGED
@@ -1,3 +1,26 @@
1
+ v1.1
2
+ New methods:
3
+ -- Added Batch API support (thanks, seejohnrun and spiegela!)
4
+ -- includes file uploads, error handling, and FQL
5
+ -- Added GraphAPI#put_video
6
+ -- Added GraphAPI#get_comments_for_urls (thanks, amrnt!)
7
+ -- Added RestAPI#fql_multiquery, which simplifies the results (thanks, amrnt!)
8
+ -- HTTP services support global proxy and timeout settings (thanks, itchy!)
9
+ -- Net::HTTP supports global ca_path, ca_file, and verify_mode settings (thanks, spiegela!)
10
+ Updated methods:
11
+ -- RealtimeUpdates now uses a GraphAPI object instead of its own API
12
+ -- RestAPI#rest_call now has an optional last argument for method, for calls requiring POST, DELETE, etc. (thanks, sshilo!)
13
+ -- Filename can now be specified when uploading (e.g. for Ads API) (thanks, sshilo!)
14
+ -- get_objects([]) returns [] instead of a Facebook error in non-batch mode (thanks, aselder!)
15
+ Internal improvements:
16
+ -- Koala is now more compatible with other Rubies (JRuby, Rubinius, etc.)
17
+ -- HTTP services are more modular and can be changed on the fly (thanks, chadk!)
18
+ -- Includes support for uploading StringIOs and other non-files via Net::HTTP even when using TyphoeusService
19
+ -- Koala now uses multi_json to improve compatibility with Rubinius and other Ruby versions
20
+ -- Koala now uses the modern Typhoeus API (thanks, aselder!)
21
+ -- Koala now uses the current modern Net::HTTP interface (thanks, romanbsd!)
22
+ -- Fixed bugs and typos (thanks, waynn, mokevnin, and tikh!)
23
+
1
24
  v1.0
2
25
  New methods:
3
26
  -- Photo and file upload now supported through #put_picture
@@ -6,13 +29,13 @@ New methods:
6
29
  -- Added put_connection and delete_connection convenience methods
7
30
  Updated methods:
8
31
  -- Search can now search places, checkins, etc. (thanks, rickyc!)
9
- -- You can now pass :beta => true in the http options to use Facebook's beta tier.
32
+ -- You can now pass :beta => true in the http options to use Facebook's beta tier
10
33
  -- TestUser#befriend now requires user info hashes (id and access token) due to Facebook API changes (thanks, pulsd and kbighorse!)
11
34
  -- All methods now accept an http_options hash as their optional last parameter (thanks, spiegela!)
12
35
  -- url_for_oauth_code can now take a :display option (thanks, netbe!)
13
36
  -- Net::HTTP can now accept :timeout and :proxy options (thanks, gilles!)
14
37
  -- Test users now supports using test accounts across multiple apps
15
- Internal improvements:
38
+ Internal improvements:
16
39
  -- For public requests, Koala now uses http by default (instead of https) to improve speed
17
40
  -- This can be overridden through Koala.always_use_ssl= or by passing :use_ssl => true in the options hash for an api call
18
41
  -- Read-only REST API requests now go through the faster api-read server
@@ -22,6 +45,7 @@ Internal improvements:
22
45
  -- Updated parse_signed_request to match Facebook's current implementation (thanks, imajes!)
23
46
  -- APIError is now < StandardError, not Exception
24
47
  -- Added KoalaError for non-API errors
48
+ -- Net::HTTP's SSL verification is no longer disabled by default
25
49
  Test improvements:
26
50
  -- Incorporated joshk's awesome rewrite of the entire Koala test suite (thanks, joshk!)
27
51
  -- Expanded HTTP service tests (added Typhoeus test suite and additional Net::HTTP test cases)
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source :rubygems
2
2
 
3
3
  gemspec
4
+
5
+ if defined? JRUBY_VERSION
6
+ gem "jruby-openssl"
7
+ end
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/koala.gemspec CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{koala}
5
- s.version = "1.0.0"
6
- s.date = %q{2011-05-01}
5
+ s.version = "1.1.0"
6
+ s.date = %q{2011-07-18}
7
7
 
8
8
  s.summary = %q{A lightweight, flexible library for Facebook with support for the Graph API, the REST API, realtime updates, and OAuth authentication.}
9
9
  s.description = %q{Koala is a lightweight, flexible Ruby SDK for Facebook. It allows read/write access to the social graph via the Graph and REST APIs, as well as support for realtime updates and OAuth and Facebook Connect authentication. Koala is fully tested and supports Net::HTTP and Typhoeus connections out of the box and can accept custom modules for other services.}
@@ -28,22 +28,22 @@ Gem::Specification.new do |s|
28
28
  s.specification_version = 3
29
29
 
30
30
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
31
- s.add_runtime_dependency(%q<json>, ["~> 1.0"])
31
+ s.add_runtime_dependency(%q<multi_json>, ["~> 1.0"])
32
32
  s.add_runtime_dependency(%q<multipart-post>, ["~> 1.0"])
33
- s.add_development_dependency(%q<rspec>, ["~> 2.5.0"])
33
+ s.add_development_dependency(%q<rspec>, ["~> 2.5"])
34
34
  s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
35
35
  s.add_development_dependency(%q<typhoeus>, ["~> 0.2.4"])
36
36
  else
37
- s.add_dependency(%q<json>, ["~> 1.0"])
37
+ s.add_dependency(%q<multi_json>, ["~> 1.0"])
38
38
  s.add_dependency(%q<multipart-post>, ["~> 1.0"])
39
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
39
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
40
40
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
41
41
  s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
42
42
  end
43
43
  else
44
- s.add_dependency(%q<json>, ["~> 1.0"])
44
+ s.add_dependency(%q<multi_json>, ["~> 1.0"])
45
45
  s.add_dependency(%q<multipart-post>, ["~> 1.0"])
46
- s.add_dependency(%q<rspec>, ["~> 2.5.0"])
46
+ s.add_dependency(%q<rspec>, ["~> 2.5"])
47
47
  s.add_dependency(%q<rake>, ["~> 0.8.7"])
48
48
  s.add_dependency(%q<typhoeus>, ["~> 0.2.4"])
49
49
  end
@@ -0,0 +1,74 @@
1
+ module Koala
2
+ module Facebook
3
+ class BatchOperation
4
+ attr_reader :access_token, :http_options, :post_processing, :files, :batch_api, :identifier
5
+
6
+ @identifier = 0
7
+
8
+ def self.next_identifier
9
+ @identifier += 1
10
+ end
11
+
12
+ def initialize(options = {})
13
+ @identifier = self.class.next_identifier
14
+ @args = (options[:args] || {}).dup # because we modify it below
15
+ @access_token = options[:access_token]
16
+ @http_options = (options[:http_options] || {}).dup # dup because we modify it below
17
+ @batch_args = @http_options.delete(:batch_args) || {}
18
+ @url = options[:url]
19
+ @method = options[:method].to_sym
20
+ @post_processing = options[:post_processing]
21
+
22
+ process_binary_args
23
+
24
+ raise Koala::KoalaError, "Batch operations require an access token, none provided." unless @access_token
25
+ end
26
+
27
+ def to_batch_params(main_access_token)
28
+ # set up the arguments
29
+ args_string = Koala.http_service.encode_params(@access_token == main_access_token ? @args : @args.merge(:access_token => @access_token))
30
+
31
+ response = {
32
+ :method => @method.to_s,
33
+ :relative_url => @url,
34
+ }
35
+
36
+ # handle batch-level arguments, such as name, depends_on, and attached_files
37
+ @batch_args[:attached_files] = @files.keys.join(",") if @files
38
+ response.merge!(@batch_args) if @batch_args
39
+
40
+ # for get and delete, we append args to the URL string
41
+ # otherwise, they go in the body
42
+ if args_string.length > 0
43
+ if args_in_url?
44
+ response[:relative_url] += (@url =~ /\?/ ? "&" : "?") + args_string if args_string.length > 0
45
+ else
46
+ response[:body] = args_string if args_string.length > 0
47
+ end
48
+ end
49
+
50
+ response
51
+ end
52
+
53
+ protected
54
+
55
+ def process_binary_args
56
+ # collect binary files
57
+ @args.each_pair do |key, value|
58
+ if UploadableIO.binary_content?(value)
59
+ @files ||= {}
60
+ # we use a class-level counter to ensure unique file identifiers across multiple batch operations
61
+ # (this is thread safe, since we just care about uniqueness)
62
+ # so remove the file from the original hash and add it to the file store
63
+ id = "op#{identifier}_file#{@files.keys.length}"
64
+ @files[id] = @args.delete(key).is_a?(UploadableIO) ? value : UploadableIO.new(value)
65
+ end
66
+ end
67
+ end
68
+
69
+ def args_in_url?
70
+ @method == :get || @method == :delete
71
+ end
72
+ end
73
+ end
74
+ end
@@ -28,7 +28,7 @@ module Koala
28
28
  # If you are using the JavaScript SDK, you can use the
29
29
  # Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
30
30
  # for the active user from the cookie saved by the SDK.
31
-
31
+
32
32
  # Objects
33
33
 
34
34
  def get_object(id, args = {}, options = {})
@@ -37,10 +37,10 @@ module Koala
37
37
  end
38
38
 
39
39
  def get_objects(ids, args = {}, options = {})
40
- # Fetchs all of the given object from the graph.
41
- # We return a map from ID to object. If any of the IDs are invalid,
42
- # we raise an exception.
43
- graph_call("", args.merge("ids" => ids.join(",")), "get", options)
40
+ # Fetchs all of the given objects from the graph.
41
+ # If any of the IDs are invalid, they'll raise an exception.
42
+ return [] if ids.empty?
43
+ graph_call("", args.merge("ids" => ids.respond_to?(:join) ? ids.join(",") : ids), "get", options)
44
44
  end
45
45
 
46
46
  def put_object(parent_object, connection_name, args = {}, options = {})
@@ -71,10 +71,19 @@ module Koala
71
71
 
72
72
  def get_connections(id, connection_name, args = {}, options = {})
73
73
  # Fetchs the connections for given object.
74
- result = graph_call("#{id}/#{connection_name}", args, "get", options)
75
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
74
+ graph_call("#{id}/#{connection_name}", args, "get", options) do |result|
75
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
76
+ end
76
77
  end
77
78
 
79
+ def get_comments_for_urls(urls = [], args = {}, options = {})
80
+ # Fetchs the comments for given URLs (array or comma-separated string)
81
+ # see https://developers.facebook.com/blog/post/490
82
+ return [] if urls.empty?
83
+ args.merge!(:ids => urls.respond_to?(:join) ? urls.join(",") : urls)
84
+ get_object("comments", args, options)
85
+ end
86
+
78
87
  def put_connections(id, connection_name, args = {}, options = {})
79
88
  # Posts a certain connection
80
89
  raise APIError.new({"type" => "KoalaMissingAccessToken", "message" => "Write operations require an access token"}) unless @access_token
@@ -87,44 +96,41 @@ module Koala
87
96
  graph_call("#{id}/#{connection_name}", args, "delete", options)
88
97
  end
89
98
 
90
- # Pictures
91
- # to delete pictures, use delete_object(photo_id)
92
- # note: you'll need the user_photos permission to actually access photos after uploading them
99
+ # Media (photos and videos)
100
+ # to delete photos or videos, use delete_object(object_id)
101
+ # note: you'll need the user_photos or user_videos permissions to actually access media after upload
93
102
 
94
103
  def get_picture(object, args = {}, options = {})
95
104
  # Gets a picture object, returning the URL (which Facebook sends as a header)
96
- result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers))
97
- result["Location"]
105
+ graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
106
+ result["Location"]
107
+ end
98
108
  end
99
109
 
110
+ # Can be called in multiple ways:
111
+ #
112
+ # put_picture(file, [content_type], ...)
113
+ # put_picture(path_to_file, [content_type], ...)
114
+ #
115
+ # You can pass in uploaded files directly from Rails or Sinatra.
116
+ # (See lib/koala/uploadable_io.rb for supported frameworks)
117
+ #
118
+ # Optional parameters can be added to the end of the argument list:
119
+ # - args: a hash of request parameters (default: {})
120
+ # - target_id: ID of the target where to post the picture (default: "me")
121
+ # - options: a hash of http options passed to the HTTPService module
122
+ #
123
+ # put_picture(file, content_type, {:message => "Message"}, 01234560)
124
+ # put_picture(params[:file], {:message => "Message"})
125
+
100
126
  def put_picture(*picture_args)
101
- # Can be called in multiple ways:
102
- #
103
- # put_picture(file, [content_type], ...)
104
- # put_picture(path_to_file, [content_type], ...)
105
- #
106
- # You can pass in uploaded files directly from Rails or Sinatra.
107
- # (See lib/koala/uploadable_io.rb for supported frameworks)
108
- #
109
- # Optional parameters can be added to the end of the argument list:
110
- # - args: a hash of request parameters (default: {})
111
- # - target_id: ID of the target where to post the picture (default: "me")
112
- # - options: a hash of http options passed to the HTTPService module
113
- #
114
- # put_picture(file, content_type, {:message => "Message"}, 01234560)
115
- # put_picture(params[:file], {:message => "Message"})
116
-
117
- raise KoalaError.new("Wrong number of arguments for put_picture") unless picture_args.size.between?(1, 5)
118
-
119
- args_offset = picture_args[1].kind_of?(Hash) || picture_args.size == 1 ? 0 : 1
120
-
121
- args = picture_args[1 + args_offset] || {}
122
- target_id = picture_args[2 + args_offset] || "me"
123
- options = picture_args[3 + args_offset] || {}
124
-
125
- args["source"] = Koala::UploadableIO.new(*picture_args.slice(0, 1 + args_offset))
127
+ put_object(*parse_media_args(picture_args, "photos"))
128
+ end
126
129
 
127
- self.put_object(target_id, "photos", args, options)
130
+ def put_video(*video_args)
131
+ args = parse_media_args(video_args, "videos")
132
+ args.last[:video] = true
133
+ put_object(*args)
128
134
  end
129
135
 
130
136
  # Wall posts
@@ -172,86 +178,81 @@ module Koala
172
178
 
173
179
  def search(search_terms, args = {}, options = {})
174
180
  args.merge!({:q => search_terms}) unless search_terms.nil?
175
- result = graph_call("search", args, "get", options)
176
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
177
- end
178
-
179
- # API access
180
-
181
- def graph_call(*args)
182
- # Direct access to the Facebook API
183
- # see any of the above methods for example invocations
184
- response = api(*args) do |response|
185
- # check for Graph API-specific errors
186
- if response.is_a?(Hash) && error_details = response["error"]
187
- raise APIError.new(error_details)
188
- end
181
+ graph_call("search", args, "get", options) do |result|
182
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
189
183
  end
184
+ end
190
185
 
191
- response
192
- end
193
186
 
194
187
  # GraphCollection support
195
-
196
188
  def get_page(params)
197
189
  # Pages through a set of results stored in a GraphCollection
198
190
  # Used for connections and search results
199
- result = graph_call(*params)
200
- result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
191
+ graph_call(*params) do |result|
192
+ result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
193
+ end
201
194
  end
202
195
 
203
- end
204
-
205
-
206
- class GraphCollection < Array
207
- #This class is a light wrapper for collections returned
208
- #from the Graph API.
209
- #
210
- #It extends Array to allow direct access to the data colleciton
211
- #which should allow it to drop in seamlessly.
212
- #
213
- #It also allows access to paging information and the
214
- #ability to get the next/previous page in the collection
215
- #by calling next_page or previous_page.
216
- attr_reader :paging
217
- attr_reader :api
218
196
 
219
- def initialize(response, api)
220
- super response["data"]
221
- @paging = response["paging"]
222
- @api = api
223
- end
224
-
225
- # defines methods for NEXT and PREVIOUS pages
226
- %w{next previous}.each do |this|
227
-
228
- # def next_page
229
- # def previous_page
230
- define_method "#{this.to_sym}_page" do
231
- base, args = send("#{this}_page_params")
232
- base ? @api.get_page([base, args]) : nil
197
+ # Batch API
198
+ def batch(http_options = {}, &block)
199
+ batch_client = GraphBatchAPI.new(access_token)
200
+ if block
201
+ yield batch_client
202
+ batch_client.execute(http_options)
203
+ else
204
+ batch_client
233
205
  end
234
-
235
- # def next_page_params
236
- # def previous_page_params
237
- define_method "#{this.to_sym}_page_params" do
238
- return nil unless @paging and @paging[this]
239
- parse_page_url(@paging[this])
206
+ end
207
+
208
+ def self.included(base)
209
+ base.class_eval do
210
+ def self.batch
211
+ raise NoMethodError, "The BatchAPI signature has changed (the original implementation was not thread-safe). Please see https://github.com/arsduo/koala/wiki/Batch-requests. (This message will be removed in the final 1.1 release.)"
212
+ end
213
+ end
214
+ end
215
+
216
+ # Direct access to the Facebook API
217
+ # see any of the above methods for example invocations
218
+ def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
219
+ result = api(path, args, verb, options) do |response|
220
+ error = check_response(response)
221
+ raise error if error
240
222
  end
223
+
224
+ # now process as appropriate (get picture header, make GraphCollection, etc.)
225
+ post_processing ? post_processing.call(result) : result
241
226
  end
242
227
 
243
- def parse_page_url(url)
244
- match = url.match(/.com\/(.*)\?(.*)/)
245
- base = match[1]
246
- args = match[2]
247
- params = CGI.parse(args)
248
- new_params = {}
249
- params.each_pair do |key,value|
250
- new_params[key] = value.join ","
228
+ def check_response(response)
229
+ # check for Graph API-specific errors
230
+ # this returns an error, which is immediately raised (non-batch)
231
+ # or added to the list of batch results (batch)
232
+ if response.is_a?(Hash) && error_details = response["error"]
233
+ APIError.new(error_details)
251
234
  end
252
- [base,new_params]
253
235
  end
254
236
 
237
+ private
238
+
239
+ def parse_media_args(media_args, method)
240
+ # photo and video uploads can accept different types of arguments (see above)
241
+ # so here, we parse the arguments into a form directly usable in put_object
242
+ raise KoalaError.new("Wrong number of arguments for put_#{method == "photos" ? "picture" : "video"}") unless media_args.size.between?(1, 5)
243
+
244
+ args_offset = media_args[1].kind_of?(Hash) || media_args.size == 1 ? 0 : 1
245
+
246
+ args = media_args[1 + args_offset] || {}
247
+ target_id = media_args[2 + args_offset] || "me"
248
+ options = media_args[3 + args_offset] || {}
249
+
250
+ args["source"] = Koala::UploadableIO.new(*media_args.slice(0, 1 + args_offset))
251
+
252
+ options[:http_service] = Koala.base_http_service if args["source"].requires_base_http_service
253
+
254
+ [target_id, method, args, options]
255
+ end
255
256
  end
256
257
  end
257
- end
258
+ end
@@ -0,0 +1,87 @@
1
+ module Koala
2
+ module Facebook
3
+ module GraphBatchAPIMethods
4
+
5
+ def self.included(base)
6
+ base.class_eval do
7
+ alias_method :graph_call_outside_batch, :graph_call
8
+ alias_method :graph_call, :graph_call_in_batch
9
+
10
+ alias_method :check_graph_api_response, :check_response
11
+ alias_method :check_response, :check_graph_batch_api_response
12
+ end
13
+ end
14
+
15
+ def batch_calls
16
+ @batch_calls ||= []
17
+ end
18
+
19
+ def graph_call_in_batch(path, args = {}, verb = "get", options = {}, &post_processing)
20
+ # for batch APIs, we queue up the call details (incl. post-processing)
21
+ batch_calls << BatchOperation.new(
22
+ :url => path,
23
+ :args => args,
24
+ :method => verb,
25
+ :access_token => options['access_token'] || access_token,
26
+ :http_options => options,
27
+ :post_processing => post_processing
28
+ )
29
+ nil # batch operations return nothing immediately
30
+ end
31
+
32
+ def check_graph_batch_api_response(response)
33
+ if response.is_a?(Hash) && response["error"] && !response["error"].is_a?(Hash)
34
+ APIError.new("type" => "Error #{response["error"]}", "message" => response["error_description"])
35
+ else
36
+ check_graph_api_response(response)
37
+ end
38
+ end
39
+
40
+ def execute(http_options = {})
41
+ return [] unless batch_calls.length > 0
42
+ # Turn the call args collected into what facebook expects
43
+ args = {}
44
+ args["batch"] = MultiJson.encode(batch_calls.map { |batch_op|
45
+ args.merge!(batch_op.files) if batch_op.files
46
+ batch_op.to_batch_params(access_token)
47
+ })
48
+
49
+ graph_call_outside_batch('/', args, 'post', http_options) do |response|
50
+ # map the results with post-processing included
51
+ index = 0 # keep compat with ruby 1.8 - no with_index for map
52
+ response.map do |call_result|
53
+ # Get the options hash
54
+ batch_op = batch_calls[index]
55
+ index += 1
56
+
57
+ if call_result
58
+ # (see note in regular api method about JSON parsing)
59
+ body = MultiJson.decode("[#{call_result['body'].to_s}]")[0]
60
+
61
+ unless call_result["code"].to_i >= 500 || error = check_response(body)
62
+ # Get the HTTP component they want
63
+ data = case batch_op.http_options[:http_component]
64
+ when :status
65
+ call_result["code"].to_i
66
+ when :headers
67
+ # facebook returns the headers as an array of k/v pairs, but we want a regular hash
68
+ call_result['headers'].inject({}) { |headers, h| headers[h['name']] = h['value']; headers}
69
+ else
70
+ body
71
+ end
72
+
73
+ # process it if we are given a block to process with
74
+ batch_op.post_processing ? batch_op.post_processing.call(data) : data
75
+ else
76
+ error || APIError.new({"type" => "HTTP #{call_result["code"].to_s}", "message" => "Response body: #{body}"})
77
+ end
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,54 @@
1
+ module Koala
2
+ module Facebook
3
+ class GraphCollection < Array
4
+ # This class is a light wrapper for collections returned
5
+ # from the Graph API.
6
+ #
7
+ # It extends Array to allow direct access to the data colleciton
8
+ # which should allow it to drop in seamlessly.
9
+ #
10
+ # It also allows access to paging information and the
11
+ # ability to get the next/previous page in the collection
12
+ # by calling next_page or previous_page.
13
+ attr_reader :paging
14
+ attr_reader :api
15
+
16
+ def initialize(response, api)
17
+ super response["data"]
18
+ @paging = response["paging"]
19
+ @api = api
20
+ end
21
+
22
+ # defines methods for NEXT and PREVIOUS pages
23
+ %w{next previous}.each do |this|
24
+
25
+ # def next_page
26
+ # def previous_page
27
+ define_method "#{this.to_sym}_page" do
28
+ base, args = send("#{this}_page_params")
29
+ base ? @api.get_page([base, args]) : nil
30
+ end
31
+
32
+ # def next_page_params
33
+ # def previous_page_params
34
+ define_method "#{this.to_sym}_page_params" do
35
+ return nil unless @paging and @paging[this]
36
+ parse_page_url(@paging[this])
37
+ end
38
+ end
39
+
40
+ def parse_page_url(url)
41
+ match = url.match(/.com\/(.*)\?(.*)/)
42
+ base = match[1]
43
+ args = match[2]
44
+ params = CGI.parse(args)
45
+ new_params = {}
46
+ params.each_pair do |key,value|
47
+ new_params[key] = value.join ","
48
+ end
49
+ [base,new_params]
50
+ end
51
+
52
+ end
53
+ end
54
+ end