require 'erb' module Recurly class Resource # Pages through an index resource, yielding records as it goes. It's rare # to instantiate one on its own: use {Resource.paginate}, # {Resource.find_each}, and Resource#{has_many_association} # instead. # # Because pagers handle +has_many+ associations, pagers can also build and # create child records. # # @example Through a resource class: # Recurly::Account.paginate # => # # # Recurly::Account.find_each { |a| p a } # @example Through an resource instance: # account.transactions # # => # # # account.transactions.new(attributes) # or #create, or #create! # # => # class Pager include Enumerable # @return [Resource] The resource class of the pager. attr_reader :resource_class # @return [Hash, nil] A hash of links to which the pager can page. attr_reader :links # @return [String, nil] An ETag for the current page. attr_reader :etag # A pager for paginating through resource records. # # @param resource_class [Resource] The resource to be paginated. # @param options [Hash] A hash of pagination options. # @option options [Integer] :per_page The number of records returned per # page. # @option options [DateTime, Time, Integer] :cursor A timestamp that the # pager will skim back to and return records created before it. # @option options [String] :etag When set, will raise {API::NotModified} # if the loaded page content has not changed. # @option options [String] :uri The default location the pager will # request. # @option options [String, Symbol] :sort The attribute that will be used to order # records: created_at, updated_at. Defaults to created_at. # @option options [String, Symbol] :order The order in which records will be # returned: asc for ascending order, desc for descending order. # Defaults to desc. # @option options [DateTime, String] :begin_time Operates on the attribute specified by the # sort parameter. Filters records to only include those with datetimes # greater than or equal to the supplied datetime. Accepts an ISO 8601 # date or date and time. # @option options [DateTime, String] :end_time Operates on the attribute specified by # the sort parameter. Filters records to only include those with # datetimes less than or equal to the supplied datetime. Accepts an # ISO 8601 date or date and time. # @raise [API::NotModified] If the :etag option is set and # matches the server's. def initialize resource_class, options = {} options[:cursor] &&= options[:cursor].to_i @parent = options.delete :parent @uri = options.delete :uri @etag = options.delete :etag @resource_class, @options = resource_class, options @collection = nil end # @return [Boolean] whether or not the xml element is present def any? !@uri.nil? end # @return [String] The URI of the paginated resource. def uri @uri ||= resource_class.collection_path end # @return [Integer] The total record count of the resource in question. # @see Resource.count def count API.head(uri, @options)['X-Records'].to_i end # @return [Array] Iterates through the current page of records. # @yield [record] def each return enum_for :each unless block_given? load! unless @collection @collection.each { |record| yield record } end # @return [nil] # @see Resource.find_each # @yield [record] def find_each return enum_for :find_each unless block_given? begin each { |record| yield record } end while self.next end # @return [Array, nil] Refreshes the pager's collection of records with # the next page. def next load_from links['next'], nil if links.key? 'next' end # @return [Array, nil] Refreshes the pager's collection of records with # the previous page. def prev load_from links['prev'], nil if links.key? 'prev' end # @return [Array, nil] Refreshes the pager's collection of records with # the first page. def start load_from links['start'], nil if links.key? 'start' end # @return [Array, nil] Load (or reload) the pager's collection from the # original, supplied options. def load! load_from uri, @options end alias reload load! # @return [Pager] Duplicates the pager, updating it with the options # supplied. Useful for resource scopes. # @see #initialize # @example # Recurly::Account.active.paginate :per_page => 20 def paginate options = {} dup.instance_eval { @collection = @etag = nil @options = @options.merge options self } end alias scoped paginate alias where paginate def all options = {} paginate(options).to_a end # Instantiates a new record in the scope of the pager. # # @return [Resource] A new record. # @example # account = Recurly::Account.find 'schrader' # subscription = account.subscriptions.new attributes # @see Resource.new def new attributes = {} record = resource_class.send(:new, attributes) { |r| r.attributes[@parent.class.member_name] ||= @parent if @parent r.uri = uri } yield record if block_given? record end # Instantiates and saves a record in the scope of the pager. # # @return [Resource] The record. # @raise [Transaction::Error] A monetary transaction failed. # @example # account = Recurly::Account.find 'schrader' # subscription = account.subscriptions.create attributes # @see Resource.create def create attributes = {} new(attributes) { |record| record.save } end # Instantiates a record in the scope of the pager. # # @return [Resource] The record. # @example # account = Recurly::Account.find 'schrader' # subscription = account.subscriptions.build attributes # @see Resource.new def build attributes = {} new(attributes) end # Instantiates and saves a record in the scope of the pager. # # @return [Resource] The saved record. # @raise [Invalid] The record is invalid. # @raise [Transaction::Error] A monetary transaction failed. # @example # account = Recurly::Account.find 'schrader' # subscription = account.subscriptions.create! attributes # @see Resource.create! def create! attributes = {} new(attributes) { |record| record.save! } end def find uuid if resource_class.respond_to? :find raise NoMethodError, "#find must be called on #{resource_class} directly" end resource_class.from_response API.get("#{uri}/#{ERB::Util.url_encode(uuid)}") end # @return [true, false] # @see Object#respond_to? def respond_to? method_name, include_private = false super || [].respond_to?(method_name, include_private) end private def load_from uri, params options = {} options[:head] = { 'If-None-Match' => etag } if etag response = API.get uri, params, options @etag = response['ETag'] @links = {} if links = response['Link'] links.scan(/<([^>]+)>; rel="([^"]+)"/).each do |link, rel| @links[rel] = link.freeze end end @links.freeze @collection = [] document = XML.new response.body document.each_element(resource_class.member_name) do |el| record = resource_class.from_xml el record.attributes[@parent.class.member_name] = @parent if @parent @collection << record end @collection.freeze rescue API::NotModified @collection and @collection or raise end def method_missing name, *args, &block scope = resource_class.scopes[name] and return paginate scope if [].respond_to? name load! unless @collection return @collection.send name, *args, &block end super end end end end