Skip to content

Commit 677fdd5

Browse files
authored
Merge pull request #536 from zendesk/fvilela/red-1497
RED-1497 Ruby Client CBP by default
2 parents a725043 + 3b14c25 commit 677fdd5

File tree

6 files changed

+257
-49
lines changed

6 files changed

+257
-49
lines changed

.rubocop.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ Layout/MultilineOperationIndentation:
3838

3939
Metrics:
4040
Enabled: false
41+
42+
Style/DoubleNegation:
43+
Enabled: false

lib/zendesk_api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ module ZendeskAPI; end
33
require 'faraday'
44
require 'faraday/multipart'
55

6+
require 'zendesk_api/helpers'
67
require 'zendesk_api/core_ext/inflection'
78
require 'zendesk_api/client'

lib/zendesk_api/collection.rb

Lines changed: 90 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ module ZendeskAPI
66
# Represents a collection of resources. Lazily loaded, resources aren't
77
# actually fetched until explicitly needed (e.g. #each, {#fetch}).
88
class Collection
9+
DEFAULT_PAGE_SIZE = 100
10+
911
include ZendeskAPI::Sideloading
1012

1113
# Options passed in that are automatically converted from an array to a comma-separated list.
@@ -186,17 +188,8 @@ def fetch!(reload = false)
186188
elsif association && association.options.parent && association.options.parent.new_record?
187189
return (@resources = [])
188190
end
189-
path_query_link = (@query || path)
190-
191-
@response = get_response(path_query_link)
192-
193-
if path_query_link == "search/export"
194-
handle_cursor_response(@response.body)
195-
else
196-
handle_response(@response.body)
197-
end
198191

199-
@resources
192+
get_resources(@query || path)
200193
end
201194

202195
def fetch(*args)
@@ -252,10 +245,12 @@ def replace(collection)
252245
# * If there is a next_page url cached, it executes a fetch on that url and returns the results.
253246
# * Otherwise, returns an empty array.
254247
def next
255-
if @options["page"]
248+
if @options["page"] && !cbp_request?
256249
clear_cache
257-
@options["page"] += 1
250+
@options["page"] = @options["page"].to_i + 1
258251
elsif (@query = @next_page)
252+
# Send _only_ url param "?after=token" to get the next page
253+
@options.page&.delete("before")
259254
fetch(true)
260255
else
261256
clear_cache
@@ -268,10 +263,12 @@ def next
268263
# * If there is a prev_page url cached, it executes a fetch on that url and returns the results.
269264
# * Otherwise, returns an empty array.
270265
def prev
271-
if @options["page"] && @options["page"] > 1
266+
if !cbp_request? && @options["page"].to_i > 1
272267
clear_cache
273268
@options["page"] -= 1
274269
elsif (@query = @prev_page)
270+
# Send _only_ url param "?before=token" to get the prev page
271+
@options.page&.delete("after")
275272
fetch(true)
276273
else
277274
clear_cache
@@ -327,21 +324,17 @@ def to_param
327324
end
328325

329326
def more_results?(response)
330-
response["meta"].present? && response["results"].present?
327+
Helpers.present?(response["meta"]) && response["meta"]["has_more"]
331328
end
332329
alias_method :has_more_results?, :more_results? # For backward compatibility with 1.33.0 and 1.34.0
333330

334-
def get_response_body(link)
335-
@client.connection.send("get", link).body
336-
end
337-
338331
def get_next_page_data(original_response_body)
339332
link = original_response_body["links"]["next"]
340-
333+
result_key = @resource_class.model_key || "results"
341334
while link
342-
response = get_response_body(link)
335+
response = @client.connection.send("get", link).body
343336

344-
original_response_body["results"] = original_response_body["results"] + response["results"]
337+
original_response_body[result_key] = original_response_body[result_key] + response[result_key]
345338

346339
link = response["meta"]["has_more"] ? response["links"]["next"] : nil
347340
end
@@ -351,17 +344,69 @@ def get_next_page_data(original_response_body)
351344

352345
private
353346

347+
def cbp_response?(body)
348+
!!(body["meta"] && body["links"])
349+
end
350+
351+
def cbp_request?
352+
@options["page"].is_a?(Hash)
353+
end
354+
355+
def intentional_obp_request?
356+
Helpers.present?(@options["page"]) && !cbp_request?
357+
end
358+
359+
def get_resources(path_query_link)
360+
if intentional_obp_request?
361+
warn "Offset Based Pagination will be deprecated soon"
362+
elsif @next_page.nil?
363+
@options_per_page_was = @options.delete("per_page")
364+
# Default to CBP by using the page param as a map
365+
@options.page = { size: (@options_per_page_was || DEFAULT_PAGE_SIZE) }
366+
end
367+
368+
begin
369+
# Try CBP first, unless the user explicitly asked for OBP
370+
@response = get_response(path_query_link)
371+
rescue ZendeskAPI::Error::NetworkError => e
372+
raise e if intentional_obp_request?
373+
# Fallback to OBP if CBP didn't work, using per_page param
374+
@options.per_page = @options_per_page_was
375+
@options.page = nil
376+
@response = get_response(path_query_link)
377+
end
378+
379+
# Keep pre-existing behaviour for search/export
380+
if path_query_link == "search/export"
381+
handle_search_export_response(@response.body)
382+
else
383+
handle_response(@response.body)
384+
end
385+
end
386+
354387
def set_page_and_count(body)
355388
@count = (body["count"] || @resources.size).to_i
356-
@next_page, @prev_page = body["next_page"], body["previous_page"]
389+
@next_page, @prev_page = page_links(body)
357390

358-
if @next_page =~ /page=(\d+)/
391+
if cbp_response?(body)
392+
# We'll delete the one we don't need in #next or #prev
393+
@options["page"]["after"] = body["meta"]["after_cursor"]
394+
@options["page"]["before"] = body["meta"]["before_cursor"]
395+
elsif @next_page =~ /page=(\d+)/
359396
@options["page"] = $1.to_i - 1
360397
elsif @prev_page =~ /page=(\d+)/
361398
@options["page"] = $1.to_i + 1
362399
end
363400
end
364401

402+
def page_links(body)
403+
if body["meta"] && body["links"]
404+
[body["links"]["next"], body["links"]["prev"]]
405+
else
406+
[body["next_page"], body["previous_page"]]
407+
end
408+
end
409+
365410
def _all(start_page = @options["page"], bang = false, &block)
366411
raise(ArgumentError, "must pass a block") unless block
367412

@@ -370,13 +415,7 @@ def _all(start_page = @options["page"], bang = false, &block)
370415

371416
while (bang ? fetch! : fetch)
372417
each do |resource|
373-
arguments = [resource, @options["page"] || 1]
374-
375-
if block.arity >= 0
376-
arguments = arguments.take(block.arity)
377-
end
378-
379-
block.call(*arguments)
418+
block.call(resource, @options["page"] || 1)
380419
end
381420

382421
last_page? ? break : self.next
@@ -422,7 +461,7 @@ def set_association_from_options
422461

423462
def get_response(path)
424463
@error = nil
425-
@response = @client.connection.send(@verb || "get", path) do |req|
464+
@client.connection.send(@verb || "get", path) do |req|
426465
opts = @options.delete_if { |_, v| v.nil? }
427466

428467
req.params.merge!(:include => @includes.join(",")) if @includes.any?
@@ -435,43 +474,39 @@ def get_response(path)
435474
end
436475
end
437476

438-
def handle_cursor_response(response_body)
439-
unless response_body.is_a?(Hash)
440-
raise ZendeskAPI::Error::NetworkError, @response.env
441-
end
477+
def handle_search_export_response(response_body)
478+
assert_valid_response_body(response_body)
442479

480+
# Note this doesn't happen in #handle_response
443481
response_body = get_next_page_data(response_body) if more_results?(response_body)
444482

445483
body = response_body.dup
446484
results = body.delete(@resource_class.model_key) || body.delete("results")
447485

448-
unless results
449-
raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}"
450-
end
486+
assert_results(results, body)
451487

452488
@resources = results.map do |res|
453489
wrap_resource(res)
454490
end
455491
end
456492

493+
# For both CBP and OBP
457494
def handle_response(response_body)
458-
unless response_body.is_a?(Hash)
459-
raise ZendeskAPI::Error::NetworkError, @response.env
460-
end
495+
assert_valid_response_body(response_body)
461496

462497
body = response_body.dup
463498
results = body.delete(@resource_class.model_key) || body.delete("results")
464499

465-
unless results
466-
raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}"
467-
end
500+
assert_results(results, body)
468501

469502
@resources = results.map do |res|
470503
wrap_resource(res)
471504
end
472505

473506
set_page_and_count(body)
474507
set_includes(@resources, @includes, body)
508+
509+
@resources
475510
end
476511

477512
# Simplified Associations#wrap_resource
@@ -514,5 +549,16 @@ def collection_method(name, *args, &block)
514549
def resource_methods
515550
@resource_methods ||= @resource_class.singleton_methods(false).map(&:to_sym)
516551
end
552+
553+
def assert_valid_response_body(response_body)
554+
unless response_body.is_a?(Hash)
555+
raise ZendeskAPI::Error::NetworkError, @response.env
556+
end
557+
end
558+
559+
def assert_results(results, body)
560+
return if results
561+
raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}"
562+
end
517563
end
518564
end

lib/zendesk_api/helpers.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
module ZendeskAPI
22
# @private
33
module Helpers
4+
def self.present?(value)
5+
![nil, false, "", " ", [], {}].include?(value)
6+
end
7+
48
# From https://github.com/rubyworks/facets/blob/master/lib/core/facets/string/modulize.rb
59
# Converts a string to module name representation.
610
#

0 commit comments

Comments
 (0)