lib/zendesk_api/collection.rb in zendesk_api-2.0.1 vs lib/zendesk_api/collection.rb in zendesk_api-3.0.0.rc1

- old
+ new

@@ -4,10 +4,12 @@ module ZendeskAPI # Represents a collection of resources. Lazily loaded, resources aren't # actually fetched until explicitly needed (e.g. #each, {#fetch}). class Collection + DEFAULT_PAGE_SIZE = 100 + include ZendeskAPI::Sideloading # Options passed in that are automatically converted from an array to a comma-separated list. SPECIALLY_JOINED_PARAMS = [:ids, :only] @@ -184,21 +186,12 @@ if @resources && (!@fetchable || !reload) return @resources elsif association && association.options.parent && association.options.parent.new_record? return (@resources = []) end - path_query_link = (@query || path) - @response = get_response(path_query_link) - - if path_query_link == "search/export" - handle_cursor_response(@response.body) - else - handle_response(@response.body) - end - - @resources + get_resources(@query || path) end def fetch(*args) fetch!(*args) rescue Faraday::ClientError => e @@ -250,14 +243,16 @@ # Find the next page. Does one of three things: # * If there is already a page number in the options hash, it increases it and invalidates the cache, returning the new page number. # * If there is a next_page url cached, it executes a fetch on that url and returns the results. # * Otherwise, returns an empty array. def next - if @options["page"] + if @options["page"] && !cbp_request? clear_cache - @options["page"] += 1 + @options["page"] = @options["page"].to_i + 1 elsif (@query = @next_page) + # Send _only_ url param "?after=token" to get the next page + @options.page&.delete("before") fetch(true) else clear_cache @resources = [] end @@ -266,14 +261,16 @@ # Find the previous page. Does one of three things: # * If there is already a page number in the options hash, it increases it and invalidates the cache, returning the new page number. # * If there is a prev_page url cached, it executes a fetch on that url and returns the results. # * Otherwise, returns an empty array. def prev - if @options["page"] && @options["page"] > 1 + if !cbp_request? && @options["page"].to_i > 1 clear_cache @options["page"] -= 1 elsif (@query = @prev_page) + # Send _only_ url param "?before=token" to get the prev page + @options.page&.delete("after") fetch(true) else clear_cache @resources = [] end @@ -325,60 +322,102 @@ def to_param map(&:to_param) end def more_results?(response) - response["meta"].present? && response["results"].present? + Helpers.present?(response["meta"]) && response["meta"]["has_more"] end alias_method :has_more_results?, :more_results? # For backward compatibility with 1.33.0 and 1.34.0 - def get_response_body(link) - @client.connection.send("get", link).body - end - def get_next_page_data(original_response_body) link = original_response_body["links"]["next"] - + result_key = @resource_class.model_key || "results" while link - response = get_response_body(link) + response = @client.connection.send("get", link).body - original_response_body["results"] = original_response_body["results"] + response["results"] + original_response_body[result_key] = original_response_body[result_key] + response[result_key] link = response["meta"]["has_more"] ? response["links"]["next"] : nil end original_response_body end private + def cbp_response?(body) + !!(body["meta"] && body["links"]) + end + + def cbp_request? + @options["page"].is_a?(Hash) + end + + def intentional_obp_request? + Helpers.present?(@options["page"]) && !cbp_request? + end + + def get_resources(path_query_link) + if intentional_obp_request? + warn "Offset Based Pagination will be deprecated soon" + elsif @next_page.nil? + @options_per_page_was = @options.delete("per_page") + # Default to CBP by using the page param as a map + @options.page = { size: (@options_per_page_was || DEFAULT_PAGE_SIZE) } + end + + begin + # Try CBP first, unless the user explicitly asked for OBP + @response = get_response(path_query_link) + rescue ZendeskAPI::Error::NetworkError => e + raise e if intentional_obp_request? + # Fallback to OBP if CBP didn't work, using per_page param + @options.per_page = @options_per_page_was + @options.page = nil + @response = get_response(path_query_link) + end + + # Keep pre-existing behaviour for search/export + if path_query_link == "search/export" + handle_search_export_response(@response.body) + else + handle_response(@response.body) + end + end + def set_page_and_count(body) @count = (body["count"] || @resources.size).to_i - @next_page, @prev_page = body["next_page"], body["previous_page"] + @next_page, @prev_page = page_links(body) - if @next_page =~ /page=(\d+)/ + if cbp_response?(body) + # We'll delete the one we don't need in #next or #prev + @options["page"]["after"] = body["meta"]["after_cursor"] + @options["page"]["before"] = body["meta"]["before_cursor"] + elsif @next_page =~ /page=(\d+)/ @options["page"] = $1.to_i - 1 elsif @prev_page =~ /page=(\d+)/ @options["page"] = $1.to_i + 1 end end + def page_links(body) + if body["meta"] && body["links"] + [body["links"]["next"], body["links"]["prev"]] + else + [body["next_page"], body["previous_page"]] + end + end + def _all(start_page = @options["page"], bang = false, &block) raise(ArgumentError, "must pass a block") unless block page(start_page) clear_cache while (bang ? fetch! : fetch) each do |resource| - arguments = [resource, @options["page"] || 1] - - if block.arity >= 0 - arguments = arguments.take(block.arity) - end - - block.call(*arguments) + block.call(resource, @options["page"] || 1) end last_page? ? break : self.next end @@ -420,11 +459,11 @@ @collection_path ||= [@resource] end def get_response(path) @error = nil - @response = @client.connection.send(@verb || "get", path) do |req| + @client.connection.send(@verb || "get", path) do |req| opts = @options.delete_if { |_, v| v.nil? } req.params.merge!(:include => @includes.join(",")) if @includes.any? if %w{put post}.include?(@verb.to_s) @@ -433,47 +472,43 @@ req.params.merge!(opts) end end end - def handle_cursor_response(response_body) - unless response_body.is_a?(Hash) - raise ZendeskAPI::Error::NetworkError, @response.env - end + def handle_search_export_response(response_body) + assert_valid_response_body(response_body) + # Note this doesn't happen in #handle_response response_body = get_next_page_data(response_body) if more_results?(response_body) body = response_body.dup results = body.delete(@resource_class.model_key) || body.delete("results") - unless results - raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}" - end + assert_results(results, body) @resources = results.map do |res| wrap_resource(res) end end + # For both CBP and OBP def handle_response(response_body) - unless response_body.is_a?(Hash) - raise ZendeskAPI::Error::NetworkError, @response.env - end + assert_valid_response_body(response_body) body = response_body.dup results = body.delete(@resource_class.model_key) || body.delete("results") - unless results - raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}" - end + assert_results(results, body) @resources = results.map do |res| wrap_resource(res) end set_page_and_count(body) set_includes(@resources, @includes, body) + + @resources end # Simplified Associations#wrap_resource def wrap_resource(res, with_association = with_association?) case res @@ -511,8 +546,19 @@ @resource_class.send(name, @client, *args, &block) end def resource_methods @resource_methods ||= @resource_class.singleton_methods(false).map(&:to_sym) + end + + def assert_valid_response_body(response_body) + unless response_body.is_a?(Hash) + raise ZendeskAPI::Error::NetworkError, @response.env + end + end + + def assert_results(results, body) + return if results + raise ZendeskAPI::Error::ClientError, "Expected #{@resource_class.model_key} or 'results' in response keys: #{body.keys.inspect}" end end end