Skip to content

Commit 2a8c364

Browse files
authored
Merge pull request #546 from zendesk/fvilela/add_cbp_support
[RED-1821] Remove CBP attempt and specify endpoints that support CBP
2 parents b6bf1fb + a1d8cc5 commit 2a8c364

File tree

5 files changed

+105
-52
lines changed

5 files changed

+105
-52
lines changed

lib/zendesk_api/client.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,25 @@ class Client
3737
def method_missing(method, *args, &block)
3838
method = method.to_s
3939
options = args.last.is_a?(Hash) ? args.pop : {}
40-
4140
unless config.use_resource_cache
42-
raise "Resource for #{method} does not exist" unless method_as_class(method)
43-
return ZendeskAPI::Collection.new(self, method_as_class(method), options)
41+
resource_class = resource_class_for(method)
42+
raise "Resource for #{method} does not exist" unless resource_class
43+
return ZendeskAPI::Collection.new(self, resource_class, options)
4444
end
4545

4646
@resource_cache[method] ||= { :class => nil, :cache => ZendeskAPI::LRUCache.new }
4747
if !options.delete(:reload) && (cached = @resource_cache[method][:cache].read(options.hash))
4848
cached
4949
else
50-
@resource_cache[method][:class] ||= method_as_class(method)
50+
@resource_cache[method][:class] ||= resource_class_for(method)
5151
raise "Resource for #{method} does not exist" unless @resource_cache[method][:class]
5252
@resource_cache[method][:cache].write(options.hash, ZendeskAPI::Collection.new(self, @resource_cache[method][:class], options))
5353
end
5454
end
5555

5656
def respond_to?(method, *args)
57-
((cache = @resource_cache[method]) && cache[:class]) || !method_as_class(method).nil? || super
57+
cache = @resource_cache[method]
58+
!!(cache.to_h[:class] || resource_class_for(method) || super)
5859
end
5960

6061
# Returns the current user (aka me)
@@ -184,9 +185,9 @@ def build_connection
184185

185186
private
186187

187-
def method_as_class(method)
188-
klass_as_string = ZendeskAPI::Helpers.modulize_string(Inflection.singular(method.to_s.gsub(/\W/, '')))
189-
ZendeskAPI::Association.class_from_namespace(klass_as_string)
188+
def resource_class_for(method)
189+
resource_name = ZendeskAPI::Helpers.modulize_string(Inflection.singular(method.to_s.gsub(/\W/, '')))
190+
ZendeskAPI::Association.class_from_namespace(resource_name)
190191
end
191192

192193
def check_url

lib/zendesk_api/collection.rb

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,14 @@ def cbp_response?(body)
348348
!!(body["meta"] && body["links"])
349349
end
350350

351+
def set_cbp_options
352+
@options_per_page_was = @options.delete("per_page")
353+
# Default to CBP by using the page param as a map
354+
@options.page = { size: (@options_per_page_was || DEFAULT_PAGE_SIZE) }
355+
end
356+
357+
# CBP requests look like: `/resources?page[size]=100`
358+
# OBP requests look like: `/resources?page=2`
351359
def cbp_request?
352360
@options["page"].is_a?(Hash)
353361
end
@@ -356,30 +364,23 @@ def intentional_obp_request?
356364
Helpers.present?(@options["page"]) && !cbp_request?
357365
end
358366

359-
def should_try_cbp?(path_query_link)
360-
not_supported_endpoints = %w[show_many]
361-
not_supported_endpoints.none? { |endpoint| path_query_link.end_with?(endpoint) }
367+
def supports_cbp?
368+
@resource_class.const_defined?(:CBP_ACTIONS) && @resource_class.const_get(:CBP_ACTIONS).any? { |supported_path| path.end_with?(supported_path) }
369+
end
370+
371+
def first_cbp_request?
372+
# @next_page will be nil when making the first cbp request
373+
@next_page.nil?
362374
end
363375

364376
def get_resources(path_query_link)
365377
if intentional_obp_request?
366378
warn "Offset Based Pagination will be deprecated soon"
367-
elsif @next_page.nil? && should_try_cbp?(path_query_link)
368-
@options_per_page_was = @options.delete("per_page")
369-
# Default to CBP by using the page param as a map
370-
@options.page = { size: (@options_per_page_was || DEFAULT_PAGE_SIZE) }
371-
end
372-
373-
begin
374-
# Try CBP first, unless the user explicitly asked for OBP
375-
@response = get_response(path_query_link)
376-
rescue ZendeskAPI::Error::NetworkError => e
377-
raise e if intentional_obp_request?
378-
# Fallback to OBP if CBP didn't work, using per_page param
379-
@options.per_page = @options_per_page_was
380-
@options.page = nil
381-
@response = get_response(path_query_link)
379+
elsif supports_cbp? && first_cbp_request?
380+
# only set cbp options if it's the first request, otherwise the options would be already in place
381+
set_cbp_options
382382
end
383+
@response = get_response(path_query_link)
383384

384385
# Keep pre-existing behaviour for search/export
385386
if path_query_link == "search/export"
@@ -394,9 +395,7 @@ def set_page_and_count(body)
394395
@next_page, @prev_page = page_links(body)
395396

396397
if cbp_response?(body)
397-
# We'll delete the one we don't need in #next or #prev
398-
@options["page"]["after"] = body["meta"]["after_cursor"]
399-
@options["page"]["before"] = body["meta"]["before_cursor"]
398+
set_cbp_response_options(body)
400399
elsif @next_page =~ /page=(\d+)/
401400
@options["page"] = $1.to_i - 1
402401
elsif @prev_page =~ /page=(\d+)/
@@ -541,9 +540,13 @@ def array_method(name, *args, &block)
541540
to_a.public_send(name, *args, &block)
542541
end
543542

543+
# If you call client.tickets.foo - and foo is not an attribute nor an association, it ends up here, as a new collection
544544
def next_collection(name, *args, &block)
545545
opts = args.last.is_a?(Hash) ? args.last : {}
546-
opts.merge!(:collection_path => @collection_path.dup.push(name))
546+
opts.merge!(collection_path: [*@collection_path, name], page: nil)
547+
# why page: nil ?
548+
# when you do client.tickets.fetch followed by client.tickets.foos => the request to /tickets/foos will
549+
# have the options page set to whatever the last options were for the tickets collection
547550
self.class.new(@client, @resource_class, @options.merge(opts))
548551
end
549552

@@ -565,5 +568,16 @@ def assert_results(results, body)
565568
return if results
566569
raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}"
567570
end
571+
572+
def set_cbp_response_options(body)
573+
@options.page = {} unless cbp_request?
574+
# the line above means an intentional CBP request where page[size] is passed on the query
575+
# this is to cater for CBP responses where we don't specify page[size] but the endpoint
576+
# responds CBP by default. i.e `client.trigger_categories.fetch`
577+
@options.page.merge!(
578+
before: body["meta"]["before_cursor"],
579+
after: body["meta"]["after_cursor"]
580+
)
581+
end
568582
end
569583
end

lib/zendesk_api/resources.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,8 @@ class Ticket < Resource
378378
extend UpdateMany
379379
extend DestroyMany
380380

381+
CBP_ACTIONS = %w[tickets].freeze
382+
381383
# Unlike other attributes, "comment" is not a property of the ticket,
382384
# but is used as a "comment on save", so it should be kept unchanged,
383385
# See https://github.com/zendesk/zendesk_api_client_rb/issues/321

spec/core/collection_spec.rb

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626
let(:response_page1) { double("response_page1", body: response_body_page1) }
2727

2828
before do
29-
allow(subject).to receive(:get_response).with("test_resources").and_return(response_page1)
29+
stub_json_request(:get, %r{test_resources}, json(response_body_page1))
3030
end
3131

3232
context "when CBP is used" do
3333
it "is empty on the first page" do
34-
subject.fetch
35-
36-
expect(subject.prev).to eq([])
34+
stub_const("ZendeskAPI::TestResource::CBP_ACTIONS", %w[test_resources])
35+
collection = ZendeskAPI::Collection.new(client, ZendeskAPI::TestResource)
36+
expect(collection.prev).to eq([])
3737
end
3838
end
3939
end
@@ -543,6 +543,34 @@ def to_proc
543543
expect(subject.fetch(true)).to be_empty
544544
end
545545
end
546+
547+
context "when the request returns a Cursor Based Pagination type of response" do
548+
let(:cbp_response) do
549+
{
550+
"meta" => {
551+
"has_more" => true,
552+
"after_cursor" => 'after_cursor',
553+
"before_cursor" => 'before_cursor'
554+
},
555+
"links" => {
556+
"next" => "next_page",
557+
"prev" => "previous_page"
558+
},
559+
"test_resources" => [{ "id" => 1 }]
560+
}
561+
end
562+
before do
563+
stub_json_request(:get, %r{test_resources}, json(cbp_response))
564+
end
565+
566+
it "sets the next and previous pages and cursors" do
567+
subject.fetch
568+
expect(subject.instance_variable_get(:@next_page)).to eq("next_page")
569+
expect(subject.instance_variable_get(:@prev_page)).to eq("previous_page")
570+
expect(subject.options.page.after).to eq("after_cursor")
571+
expect(subject.options.page.before).to eq("before_cursor")
572+
end
573+
end
546574
end
547575

548576
context "save" do
@@ -1015,8 +1043,9 @@ def to_proc
10151043
end
10161044
end
10171045

1018-
context "when fetching a collection" do
1046+
context "when fetching a collection that supports CBP" do
10191047
before do
1048+
stub_const("ZendeskAPI::TestResource::CBP_ACTIONS", %w[test_resources])
10201049
expect(subject).to receive(:get_response).with("test_resources").and_return(cbp_success_response)
10211050
end
10221051

@@ -1033,18 +1062,6 @@ def to_proc
10331062
end
10341063
end
10351064

1036-
context "when the endpoint does not support CBP" do
1037-
before do
1038-
expect(subject).to receive(:get_response).with("test_resources").and_raise(ZendeskAPI::Error::NetworkError).twice
1039-
end
1040-
1041-
it "tries an OBP request after a failed CBP request" do
1042-
subject.per_page(2).fetch
1043-
expect(subject.instance_variable_get(:@options)["per_page"]).to eq(2)
1044-
expect(subject.instance_variable_get(:@options)["page"]).to be_nil
1045-
end
1046-
end
1047-
10481065
describe "#all going through multiple pages" do
10491066
def generate_response(index, has_more)
10501067
{
@@ -1061,6 +1078,7 @@ def generate_response(index, has_more)
10611078
}
10621079
end
10631080
before do
1081+
stub_const("ZendeskAPI::TestResource::CBP_ACTIONS", %w[test_resources])
10641082
stub_json_request(:get, %r{test_resources\?page%5Bsize%5D=1}, json(generate_response(1, true)))
10651083
stub_json_request(:get, %r{test_resources.json\/\?page%5Bafter%5D=after1&page%5Bsize%5D=1}, json(generate_response(2, true)))
10661084
stub_json_request(:get, %r{test_resources.json\/\?page%5Bafter%5D=after2&page%5Bsize%5D=1}, json(generate_response(3, true)))
@@ -1069,12 +1087,12 @@ def generate_response(index, has_more)
10691087

10701088
it "fetches all pages and yields the correct arguments" do
10711089
expect do |b|
1072-
silence_logger { subject.all(&b) }
1090+
silence_logger { subject.per_page(1).all(&b) }
10731091
end.to yield_successive_args(
1074-
[ZendeskAPI::TestResource.new(client, id: 1), "after" => "after1", "before" => "before0", "size" => 100],
1075-
[ZendeskAPI::TestResource.new(client, id: 2), "after" => "after2", "before" => "before1", "size" => 100],
1076-
[ZendeskAPI::TestResource.new(client, id: 3), "after" => "after3", "before" => "before2", "size" => 100],
1077-
[ZendeskAPI::TestResource.new(client, id: 4), "after" => "after4", "before" => "before3", "size" => 100]
1092+
[ZendeskAPI::TestResource.new(client, id: 1), "after" => "after1", "before" => "before0", "size" => 1],
1093+
[ZendeskAPI::TestResource.new(client, id: 2), "after" => "after2", "before" => "before1", "size" => 1],
1094+
[ZendeskAPI::TestResource.new(client, id: 3), "after" => "after3", "before" => "before2", "size" => 1],
1095+
[ZendeskAPI::TestResource.new(client, id: 4), "after" => "after4", "before" => "before3", "size" => 1]
10781096
)
10791097
end
10801098
end

spec/live/ticket_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,24 @@ def valid_attributes
6262
end
6363
end
6464

65+
describe "#show_many" do
66+
let(:how_many_tickets) { 10 }
67+
before do
68+
VCR.use_cassette("get_tickets_show_many") do
69+
@tickets = client.tickets.per_page(how_many_tickets).fetch
70+
end
71+
72+
VCR.use_cassette("tickets_show_many_with_ids") do
73+
@tickets_from_show_many = client.tickets.show_many(ids: @tickets.map(&:id)).fetch
74+
end
75+
end
76+
77+
it "returns the correct number of tickets" do
78+
expect(@tickets_from_show_many.count).to eq(how_many_tickets)
79+
expect(@tickets.map(&:id).sort).to eq(@tickets_from_show_many.map(&:id).sort)
80+
end
81+
end
82+
6583
describe "#attributes_for_save" do
6684
let :ticket do
6785
described_class.new(instance_double(ZendeskAPI::Client), status: :new)

0 commit comments

Comments
 (0)