# frozen_string_literal: true require_relative '../instrumentation' class WCC::Contentful::SimpleClient class Response include ::WCC::Contentful::Instrumentation attr_reader :raw_response attr_reader :client attr_reader :request delegate :status, to: :raw_response alias_method :code, :status delegate :headers, to: :raw_response def body @body ||= raw_response.body.to_s end def raw @raw ||= JSON.parse(body) end alias_method :to_json, :raw def error_message parsed_message = begin raw.dig('message') rescue JSON::ParserError nil end parsed_message || "#{code}: #{raw_response.body}" end def skip raw['skip'] end def total raw['total'] end def next_page? return unless raw.key? 'items' page_items.length + skip < total end def next_page return unless next_page? return @next_page if @next_page query = (@request[:query] || {}).merge({ skip: page_items.length + skip }) np = _instrument 'page', url: @request[:url], query: query do @client.get(@request[:url], query) end @next_page = np.assert_ok! end def initialize(client, request, raw_response) @client = client @request = request @raw_response = raw_response @body = raw_response.body.to_s end def assert_ok! return self if status >= 200 && status < 300 raise ApiError[status], self end def each_page(&block) raise ArgumentError, 'Not a collection response' unless page_items ret = Enumerator.new do |y| y << self if next_page? next_page.each_page.each do |page| y << page end end end if block_given? ret.map(&block) else ret.lazy end end def items each_page.flat_map(&:page_items) end def page_items raw['items'] end def count total end def first raise ArgumentError, 'Not a collection response' unless page_items page_items.first end def includes @includes ||= raw.dig('includes')&.each_with_object({}) do |(_t, entries), h| entries.each { |e| h[e.dig('sys', 'id')] = e } end || {} return @includes unless @next_page # This could be more efficient - maybe not worth worrying about @includes.merge(@next_page.includes) end end class SyncResponse < Response def initialize(response) super(response.client, response.request, response.raw_response) end def next_page? raw['nextPageUrl'].present? end def next_page return unless next_page? url = raw['nextPageUrl'] next_page = _instrument 'page', url: url do @client.get(url) end @next_page ||= SyncResponse.new(next_page) @next_page.assert_ok! end def next_sync_token # If we haven't grabbed the next page yet, then our next "sync" will be getting # the next page. We could just as easily call sync again with that token. @next_page&.next_sync_token || @next_sync_token ||= SyncResponse.parse_sync_token( raw['nextPageUrl'] || raw['nextSyncUrl'] ) end def each_page raise ArgumentError, 'Not a collection response' unless page_items ret = Enumerator.new do |y| y << self if next_page? next_page.each_page.each do |page| y << page end end end if block_given? ret.map(&block) else ret.lazy end end def count raise NotImplementedError, 'Sync does not return an accurate total. Use #items.count instead.' end def self.parse_sync_token(url) url = URI.parse(url) q = CGI.parse(url.query) q['sync_token']&.first end end class ApiError < StandardError attr_reader :response def self.[](code) case code when 404 NotFoundError when 401 UnauthorizedError when 429 RateLimitError else ApiError end end def initialize(response) @response = response super(response.error_message) end end class NotFoundError < ApiError end class UnauthorizedError < ApiError end class RateLimitError < ApiError end end