Skip to content

Commit

Permalink
Merge pull request #68 from seejohnrun/master
Browse files Browse the repository at this point in the history
Added Batch Processing!
  • Loading branch information
arsduo committed May 12, 2011
2 parents 8728a5f + 297cb1d commit dea3d36
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 17 deletions.
40 changes: 40 additions & 0 deletions lib/koala.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,46 @@ def api(path, args = {}, verb = "get", options = {}, &error_checking_block)
body
end
end

def batch_api(batch_calls)

# Get the access token for the user and start building a hash to store params
args = {}
args['access_token'] = @access_token || @app_access_token if @access_token || @app_access_token

# Turn the call args collected into what facebook expects
calls = batch_calls.map do |call|
{ 'method' => call[2], 'relative_url' => call[0], 'body' => call[1].map { |k, v| "#{k}=#{v}" }.join('&') }
end
args['batch'] = calls.to_json

# Make the POST request for the batch call
result = Koala.make_request('/', args, 'post')

# Raise an error if we get a 500
raise APIError.new({"type" => "HTTP #{result.status.to_s}", "message" => "Response body: #{result.body}"}) if result.status != 200

# Map the results with post-processing included
idx = 0 # keep compat with ruby 1.8 - no with_index for map
JSON.parse(result.body.to_s).map do |result|
# Get the options hash
options = batch_calls[idx][3]
idx += 1
# Get the HTTP component they want
if options[:http_component] == :headers
data = {}
result['headers'].each { |h| data[h['name']] = h['value'] }
else
body = result['body']
data = body ? JSON::parse(body) : {}
end
# Process it if we are given a block to process with
process_block = options[:process]
process_block ? process_block.call(data) : data
end

end

end

class GraphAPI < API
Expand Down
51 changes: 39 additions & 12 deletions lib/koala/graph_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ module GraphAPIMethods
# If you are using the JavaScript SDK, you can use the
# Koala::Facebook::OAuth.get_user_from_cookie() method below to get the OAuth access token
# for the active user from the cookie saved by the SDK.

# Objects

def get_object(id, args = {}, options = {})
Expand Down Expand Up @@ -72,8 +72,9 @@ def delete_object(id, options = {})

def get_connections(id, connection_name, args = {}, options = {})
# Fetchs the connections for given object.
result = graph_call("#{id}/#{connection_name}", args, "get", options)
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
graph_call("#{id}/#{connection_name}", args, "get", options) do |result|
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
end
end

def put_connections(id, connection_name, args = {}, options = {})
Expand All @@ -94,8 +95,9 @@ def delete_connections(id, connection_name, args = {}, options = {})

def get_picture(object, args = {}, options = {})
# Gets a picture object, returning the URL (which Facebook sends as a header)
result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers))
result["Location"]
graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
result["Location"]
end
end

def put_picture(*picture_args)
Expand Down Expand Up @@ -173,13 +175,38 @@ def delete_like(object_id, options = {})

def search(search_terms, args = {}, options = {})
args.merge!({:q => search_terms}) unless search_terms.nil?
result = graph_call("search", args, "get", options)
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
graph_call("search", args, "get", options) do |result|
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
end
end

# API access

def graph_call(*args)

# Make a call which may or may not be batched
def graph_call(*args, &process)
if @batch_mode
args[3][:process] = process
@batch_calls << args
else
result = non_batch_graph_call(*args)
process ? process.call(result) : result
end
end

# Wrap a block of calls in a batch, execute when finished and return results as an array
def batch(&block)
@batch_mode = true
@batch_calls = []
yield
begin
results = batch_api(@batch_calls)
ensure
@batch_mode = false
end
results
end

def non_batch_graph_call(*args)
# Direct access to the Facebook API
# see any of the above methods for example invocations
response = api(*args) do |response|
Expand All @@ -188,7 +215,6 @@ def graph_call(*args)
raise APIError.new(error_details)
end
end

response
end

Expand All @@ -197,8 +223,9 @@ def graph_call(*args)
def get_page(params)
# Pages through a set of results stored in a GraphCollection
# Used for connections and search results
result = graph_call(*params)
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
graph_call(*params) do |result|
result ? GraphCollection.new(result, self) : nil # when facebook is down nil can be returned
end
end

end
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ When retrieving data that returns an array of results (for example, when calling
# You can use those params to easily get the next (or prevous) page
page = graph.get_page(feed.next_page_params)

You can make multiple calls at once using Facebook's batch API:

# Returns an array of results as if they were called non-batch
graph.batch do
graph.get_connections('me', 'friends')
graph.get_object('me')
graph.get_picture('me')
end

Check out the wiki for more examples.

The old-school REST API
Expand Down Expand Up @@ -140,4 +149,4 @@ You can also run live tests against Facebook's servers:
# Again from anywhere in the project directory:
LIVE=true rake spec

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 thisdata 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.)
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 thisdata 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.)
12 changes: 11 additions & 1 deletion spec/fixtures/mock_facebook_responses.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ graph_api:
get:
with_token: '[{}, {}]'
no_token: '[{}, {}]'
batch=[{"method":"get","relative_url":"me","body":""},{"method":"get","relative_url":"koppel","body":""}]:
post:
with_token: '[{"body":"{\"id\":\"123\"}"}, {"body":"{\"id\":\"456\"}"}]'
batch=[{"method":"get","relative_url":"me/picture","body":""}]:
post:
with_token: '[{"headers":[{"name":"Location","value":"http://google.com"}]}]'
batch=[{"method":"get","relative_url":"me","body":""},{"method":"get","relative_url":"me/friends","body":""}]:
post:
with_token: '[{"body":"{\"id\":\"123\"}"}, {"body":"{\"data\":[]}"}]'

/me:
no_args:
get:
Expand Down Expand Up @@ -311,4 +321,4 @@ graph_api:

/777777777:
no_args:
<<: *item_deleted
<<: *item_deleted
31 changes: 28 additions & 3 deletions spec/support/graph_api_shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,31 @@
result["updated_time"].should
end

it 'should be able to get data about a user and me at the same time' do
me, koppel = @api.batch do
@api.get_object('me')
@api.get_object('koppel')
end
me['id'].should_not be_nil
koppel['id'].should_not be_nil
end

it 'should be able to make a get_picture call inside of a batch' do
pictures = @api.batch do
@api.get_picture('me')
end
pictures.first.should_not be_empty
end

it 'should be able to make mixed calls inside of a batch' do
me, friends = @api.batch do
@api.get_object('me')
@api.get_connections('me', 'friends')
end
me['id'].should_not be_nil
friends.should be_a(Array)
end

it "should be able to get multiple objects" do
result = @api.get_objects(["contextoptional", "naitik"])
result.length.should == 2
Expand Down Expand Up @@ -335,15 +360,15 @@

it "should return the previous page of results" do
@result.should_receive(:previous_page_params).and_return([@base, @args])
@api.should_receive(:graph_call).with(@base, @args).and_return(@second_page)
@api.should_receive(:graph_call).with(@base, @args).and_yield(@second_page)
Koala::Facebook::GraphCollection.should_receive(:new).with(@second_page, @api).and_return(@page_of_results)

@result.previous_page.should == @page_of_results
end

it "should return the next page of results" do
@result.should_receive(:next_page_params).and_return([@base, @args])
@api.should_receive(:graph_call).with(@base, @args).and_return(@second_page)
@api.should_receive(:graph_call).with(@base, @args).and_yield(@second_page)
Koala::Facebook::GraphCollection.should_receive(:new).with(@second_page, @api).and_return(@page_of_results)

@result.next_page.should == @page_of_results
Expand Down Expand Up @@ -426,4 +451,4 @@
it "should not be able to delete a like" do
lambda { @api.delete_like("7204941866_119776748033392") }.should raise_error(Koala::Facebook::APIError)
end
end
end

0 comments on commit dea3d36

Please sign in to comment.