module Bearcat class ApiArray include Enumerable attr_reader :raw_response, :members def self.process_response(response, api_client) if response.body.is_a?(Array) ApiArray.new(response, api_client) elsif key = array_key(response) ApiArray.new(response, api_client, key) else make_indifferent(response.body) end end def self.make_indifferent(thing) if thing.is_a?(Array) thing.map { |v| make_indifferent(v) } elsif thing.is_a?(Hash) thing.with_indifferent_access else thing end end def initialize(response, api_client, array_key = nil) @api_client = api_client @raw_response = response @array_key = array_key @page_count = nil case response.status when 200..206 @members = process_body(response) init_pages(headers[:link]) end end delegate :status, :headers, to: :raw_response def method raw_response.env[:method] end def [](i) @members[i] end def last @members.last end def each(&block) @members.each { |member| block.call(member) } end def pages? @link_hash['next'] || @link_hash['prev'] end def page_count return nil unless @link_hash['last'] uri = URI.parse(@link_hash['last']) params = CGI.parse(uri.query) params.dig('page', 0)&.to_i || 1 end %w[next prev first last].each do |rel| define_method :"#{rel}_page" do load_page(rel) end end def each_page(page_count = nil, &block) return to_enum(:each_page, page_count) unless block_given? if pages? iterate_pages(page_count) do |page_response| @members = process_body(page_response) break unless @members.present? block.call(@members) end else block.call(@members) end end def all_pages_each(page_count = nil, &block) return to_enum(:all_pages_each, page_count) unless block_given? each_page(page_count) do |page| page.each &block end end def all_pages!(page_count = nil) if pages? @members = [] iterate_pages(page_count) do |page_response| page_members = process_body(page_response) break unless page_members.present? @members.concat(page_members) end @link_hash = {} end self end private def init_pages(link_header = headers[:link]) @link_hash = {} if headers.has_key? 'Link' links = link_header.split(/,\s?/) links.each do |link| link_parts = link.split(/;\s?/) url = link_parts.shift.strip.gsub(/^<|>$/, '') rel = link_parts.find { |part| part.gsub('"', '').split('=').first == 'rel' } @link_hash[rel.gsub('"', '').split('=').last] = url end end end def get_page(url, params = {}) params['per_page'] = @page_count unless params.key?('per_page') || !@page_count parsed_url = URI.parse(url) p = parse_url_params(parsed_url) u = url.gsub("?#{parsed_url.query}", '') # merge params p.merge!(params) @api_client.connection.send(:get) do |r| r.url(u, p) end end def load_page(rel) if @link_hash.has_key? rel response = get_page(@link_hash[rel]) ApiArray.process_response(response, @api_client) end end def iterate_pages(per_page = @page_count) return to_enum(:iterate_pages, per_page) unless block_given? if per_page.present? && per_page != per_page_count && @link_hash['first'] @page_count = per_page @raw_response = response = get_page(@link_hash['first']) yield response init_pages(headers[:link]) else yield @raw_response end while @link_hash['next'] @raw_response = response = get_page(@link_hash['next']) yield response init_pages(headers[:link]) end end def per_page_count url = raw_response.env[:url] query_params = parse_url_params(url) query_params[:per_page]&.to_i end def parse_url_params(url) url = URI.parse(url) if url.is_a?(String) p = CGI.parse(url.query || '') p.default = nil # strip value out of array if value is an array and key doesn't have [] (parameter is not an array parameter) p.each { |k, v| p[k] = v.first if v.is_a?(Array) && k !~ /\[\]$/ } # remove [] from key names, this is copied from rails' {}.transform_keys! p.keys.each { |k| p[k.delete('[]')] = p.delete(k) } p.with_indifferent_access end #TODO: This is a quick fix for JSONAPI responses if we need to do this for anything else we need to do this a better way def self.array_key(response) key = nil if response.env[:method] == :get path = response.env[:url].path if path.match(/.*\/(courses||groups)\/\d+\/conferences/) key = 'conferences' elsif path.match(/.*\/accounts\/(?:(?:\d+~)?\d+|self)\/terms/) key = 'enrollment_terms' end end key end def process_body(response) if response.body.is_a?(Array) ApiArray.make_indifferent(response.body) elsif response.body.is_a?(Hash) && @array_key ApiArray.make_indifferent(response.body[@array_key]) end end end end