lib/koala/api/graph_batch_api.rb
require "koala/api"
require "koala/api/batch_operation"
module Koala
module Facebook
# @private
class GraphBatchAPI
# inside a batch call we can do anything a regular Graph API can do
include GraphAPIMethods
# Limits from @see https://developers.facebook.com/docs/marketing-api/batch-requests/v2.8
MAX_CALLS = 50
attr_reader :original_api
def initialize(api)
@original_api = api
end
def batch_calls
@batch_calls ||= []
end
# Enqueue a call into the batch for later processing.
# See API#graph_call
def graph_call(path, args = {}, verb = "get", options = {}, &post_processing)
# normalize options for consistency
options = Koala::Utils.symbolize_hash(options)
# for batch APIs, we queue up the call details (incl. post-processing)
batch_calls << BatchOperation.new(
:url => path,
:args => args,
:method => verb,
:access_token => options[:access_token] || access_token,
:http_options => options,
:post_processing => post_processing
)
nil # batch operations return nothing immediately
end
# execute the queued batch calls. limits it to 50 requests per call.
# NOTE: if you use `name` and JsonPath references, you should ensure to call `execute` for each
# co-reference group and that the group size is not greater than the above limits.
def execute(http_options = {})
return [] if batch_calls.empty?
batch_results = []
batch_calls.each_slice(MAX_CALLS) do |batch|
# Turn the call args collected into what facebook expects
args = {"batch" => batch_args(batch)}
batch.each do |call|
args.merge!(call.files || {})
end
original_api.graph_call("/", args, "post", http_options) do |response|
raise bad_response if response.nil?
batch_results += generate_results(response, batch)
end
end
batch_results
end
def generate_results(response, batch)
index = 0
response.map do |call_result|
batch_op = batch[index]
index += 1
post_process = batch_op.post_processing
# turn any results that are pageable into GraphCollections
result = result_from_response(call_result, batch_op)
# and pass to post-processing callback if given
if post_process
post_process.call(result)
else
result
end
end
end
def bad_response
# Facebook sometimes reportedly returns an empty body at times
BadFacebookResponse.new(200, "", "Facebook returned an empty body")
end
def result_from_response(response, options)
return nil if response.nil?
headers = headers_from_response(response)
error = error_from_response(response, headers)
component = options.http_options[:http_component]
error || desired_component(
component: component,
response: response,
headers: headers
)
end
def headers_from_response(response)
headers = response.fetch("headers", [])
headers.inject({}) do |compiled_headers, header|
compiled_headers.merge(header.fetch("name") => header.fetch("value"))
end
end
def error_from_response(response, headers)
code = response["code"]
body = response["body"].to_s
GraphErrorChecker.new(code, body, headers).error_if_appropriate
end
def batch_args(calls_for_batch)
calls = calls_for_batch.map do |batch_op|
batch_op.to_batch_params(access_token, app_secret)
end
JSON.dump calls
end
def json_body(response)
# quirks_mode is needed because Facebook sometimes returns a raw true or false value --
# in Ruby 2.4 we can drop that.
JSON.parse(response.fetch("body"), quirks_mode: true)
end
def desired_component(component:, response:, headers:)
result = Koala::HTTPService::Response.new(response['status'], response['body'], headers)
# Get the HTTP component they want
case component
when :status then response["code"].to_i
# facebook returns the headers as an array of k/v pairs, but we want a regular hash
when :headers then headers
# (see note in regular api method about JSON parsing)
else GraphCollection.evaluate(result, original_api)
end
end
def access_token
original_api.access_token
end
def app_secret
original_api.app_secret
end
end
end
end