module Fragmentary::Fragment::ClassMethods

Class Methods


Public Class Methods

user_type(user) click to toggle source
# File lib/fragmentary/fragment.rb, line 165
def self.user_type(user)
  (@user_type_mapping || Fragmentary.config.default_user_type_mapping).try(:call, user)
end
user_types() click to toggle source
# File lib/fragmentary/fragment.rb, line 169
def self.user_types
  @user_types || Fragmentary.config.session_users.keys
end

Public Instance Methods

acts_as_list_fragment(members:, list_record:, **options) click to toggle source

This method defines the handler for the creation of new list items. The method takes:

- members: a symbol representing the association class whose records define membership
  of the list,
- list_record: an association that when applied to a membership record identifies the record_id
  associated with the list itself. This can be specified in the form of a symbol representing
  a method to be applied to the membership association or a proc that takes the membership
  association as an argument.
# File lib/fragmentary/fragment.rb, line 321
      def acts_as_list_fragment(members:, list_record:, **options)
        # The name of the association that defines elements of the list
        @members = members.to_s.singularize
        # And the corresponding class
        @membership_class = @members.classify.constantize
        # A method (in the form of a symbol) or proc that returns the id of the record that identifies
        # the list fragment instance for a given member.
        @list_record = list_record

        # Identifies the record_ids of list fragments associated with a specific membership association.
        # This method will be called from the block passed to 'subscribe_to' below, which is executed
        # against the Subscriber, but sends missing methods back to its client, which is this class.
        # A ListFragment is not declared with 'needs_record_id'; by default it receives its record_id
        # from its parent fragment.
        def list_record(association)
          if @list_record.is_a? Symbol
            association.send @list_record
          elsif @list_record.is_a? Proc
            @list_record.call(association)
          end
        end

        if options.delete(:delay) == true
          # Note that the following assumes that @list_record is a symbol
          instance_eval <<-HEREDOC
            class #{self.name}::Create#{@membership_class}Handler < Fragmentary::Handler
              def call
                association = @args
                #{self.name}.touch_fragments_for_record(association[:#{@list_record.to_s}])
              end
            end

            subscribe_to #{@membership_class} do
              def create_#{@members}_successful(association)
                #{self.name}::Create#{@membership_class}Handler.create(association.to_h)
              end
            end
          HEREDOC
        else
          instance_eval <<-HEREDOC
            subscribe_to #{@membership_class} do
              def create_#{@members}_successful(association)
                touch_fragments_for_record(list_record(association))
              end
            end
          HEREDOC
        end

        instance_eval <<-HEREDOC
          def self.child_search_key
            :record_id
          end
        HEREDOC
      end
attributes(options) click to toggle source

Each fragment record is unique by type and parent_id (which is nil for a root_fragment) and for some types also by record_id (i.e. for root fragments for pages associated with particular AR records and for child fragments that appear in a list) user_type (e.g. “admin”, “signed_in”, “signed_out”) and user_id (for fragments that include user-specific content).

# File lib/fragmentary/fragment.rb, line 67
def attributes(options)
  klass = options.delete(:type).constantize

  # Augment the options with the user_type and user_id in case they are needed below
  options.reverse_merge!(:user_type => klass.user_type(user = options.delete(:user)), :user_id => user.try(:id))

  # Collect the attributes to be used when searching for an existing fragment. Fragments are unique by these values.
  search_attributes = {}

  parent_id = options.delete(:parent_id)
  search_attributes.merge!(:parent_id => parent_id) if parent_id

  [:record_id, :user_id, :user_type, :key].each do |attribute_name|
    if klass.needs?(attribute_name)
      option_name = (attribute_name == :key and klass.key_name) ? klass.key_name : attribute_name
      attribute = options.delete(option_name) {puts caller(0); raise ArgumentError, "Fragment type #{klass} needs a #{option_name.to_s}"}
      attribute = attribute.try :to_s if attribute_name == :key
      search_attributes.merge!(attribute_name => attribute)
    end
  end

  # If :user_id or :user_name aren't required, don't include them when we create a new fragment record.
  options.delete(:user_id); options.delete(:user_type)

  return klass, search_attributes, options
end
cache_store() click to toggle source
# File lib/fragmentary/fragment.rb, line 94
def cache_store
  @@cache_store ||= Rails.application.config.action_controller.cache_store
end
child_search_key() click to toggle source
# File lib/fragmentary/fragment.rb, line 286
def child_search_key
  nil
end
existing(options) click to toggle source

ToDo: combine this with Fragment.root

# File lib/fragmentary/fragment.rb, line 99
def existing(options)
  if fragment = options[:fragment]
    raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.existing, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
  else
    options.merge!(:type => name) unless self == base_class
    raise ArgumentError, "A 'type' attribute is needed in order to retrieve a fragment" unless options[:type]
    klass, search_attributes, options = base_class.attributes(options)
    # We merge options because it may include :record_id, which may be needed for uniqueness even
    # for classes that don't 'need_record_id' if the parent_id isn't available.
    fragment = klass.where(search_attributes.merge(options)).includes(:children).first
    # Unlike Fragment.root and Fragment#child we don't instantiate a record if none is found,
    # so fragment may be nil.
    fragment.try :set_indexed_children if fragment.try :child_search_key
  end
  fragment
end
fragment_type() click to toggle source
# File lib/fragmentary/fragment.rb, line 116
def fragment_type
  self
end
fragments_for_record(record_id) click to toggle source
# File lib/fragmentary/fragment.rb, line 278
def fragments_for_record(record_id)
  self.where(:record_id => record_id)
end
key_name() click to toggle source
# File lib/fragmentary/fragment.rb, line 183
def key_name
  @key_name ||= nil
end
list_record(association) click to toggle source

Identifies the record_ids of list fragments associated with a specific membership association. This method will be called from the block passed to 'subscribe_to' below, which is executed against the Subscriber, but sends missing methods back to its client, which is this class. A ListFragment is not declared with 'needs_record_id'; by default it receives its record_id from its parent fragment.

# File lib/fragmentary/fragment.rb, line 335
def list_record(association)
  if @list_record.is_a? Symbol
    association.send @list_record
  elsif @list_record.is_a? Proc
    @list_record.call(association)
  end
end
needs?(attribute_name) click to toggle source
# File lib/fragmentary/fragment.rb, line 140
def needs?(attribute_name)
  attribute_name = attribute_name.to_s if attribute_name.is_a? Symbol
  raise ArgumentError unless attribute_name.is_a? String
  send :"needs_#{attribute_name.to_s}?"
end
needs_key(options = {}) click to toggle source
# File lib/fragmentary/fragment.rb, line 175
def needs_key(options = {})
  extend NeedsKey
  if name = options.delete(:name) || options.delete(:key_name)
    self.key_name = name.to_sym
    define_method(key_name) {send(:key)}
  end
end
needs_key?() click to toggle source
# File lib/fragmentary/fragment.rb, line 267
def needs_key?
  false
end
needs_record_id(options = {}) click to toggle source

If a class declares 'needs_record_id', a record_id value must be provided in the attributes hash in order to either create or retrieve a Fragment of that class. Ordinarily a record_id is passed automatically from a parent fragment to its child. However if the child fragment class is declared with 'needs_record_id' the parent's record_id is not passed on and must be provided explicitly, typically for Fragment classes that represent items in a list that each correspond to a particular record of some ActiveRecord class. In these cases the record_id should be provided explicitly in the call to cache_fragment (for a root fragment) or cache_child (for a child fragment).

# File lib/fragmentary/fragment.rb, line 193
def needs_record_id(options = {})
  self.extend NeedsRecordId
  if record_type = options.delete(:record_type) || options.delete(:type)
    set_record_type(record_type)
  end
end
needs_record_id?() click to toggle source
# File lib/fragmentary/fragment.rb, line 245
def needs_record_id?
  false
end
needs_user_id() click to toggle source

If a class declares 'needs_user_id', a user_id value must be provided in the attributes hash in order to either create or retrieve a Fragment of that class. A user_id is needed for example when caching user-specific content such as a user profile. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child', a :user option is added to the options hash automatically from the value of 'current_user'. The user_id is extracted from this option in Fragment.attributes.

# File lib/fragmentary/fragment.rb, line 151
def needs_user_id
  self.extend NeedsUserId
end
needs_user_id?() click to toggle source
# File lib/fragmentary/fragment.rb, line 249
def needs_user_id?
  false
end
needs_user_type(options = {}) click to toggle source

If a class declares 'needs_user_type', a user_type value must be provided in the attributes hash in order to either create or retrieve a Fragment of that class. A user_type is needed to distinguish between fragments that are rendered differently depending on the type of user, e.g. to distinguish between content seen by signed in users and those not signed in. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child', a :user option is added to the options hash automatically from the value of 'current_user'. The user_type is extracted from this option in Fragment.attributes.

# File lib/fragmentary/fragment.rb, line 161
def needs_user_type(options = {})
  self.extend NeedsUserType
  instance_eval do
    @user_type_mapping = options[:user_type_mapping]
    def self.user_type(user)
      (@user_type_mapping || Fragmentary.config.default_user_type_mapping).try(:call, user)
    end
    @user_types = Fragmentary.parse_session_users(options[:session_users] || options[:types] || options[:user_types])
    def self.user_types
      @user_types || Fragmentary.config.session_users.keys
    end
  end
end
needs_user_type?() click to toggle source
# File lib/fragmentary/fragment.rb, line 263
def needs_user_type?
  false
end
queue_request(request=nil) click to toggle source
# File lib/fragmentary/fragment.rb, line 290
def queue_request(request=nil)
  if request
    request_queues.each{|key, queue| queue << request}
  end
end
record_type() click to toggle source
# File lib/fragmentary/fragment.rb, line 200
def record_type
  raise ArgumentError, "The #{self.name} class has no record_type" unless @record_type
  @record_type
end
remove_fragments_for_record(record_id) click to toggle source
# File lib/fragmentary/fragment.rb, line 241
def remove_fragments_for_record(record_id)
  where(:record_id => record_id).each(&:destroy)
end
remove_queued_request(user:, request_path:) click to toggle source
# File lib/fragmentary/fragment.rb, line 132
def remove_queued_request(user:, request_path:)
  request_queues[user_type(user)].remove_path(request_path)
end
request_method() click to toggle source

The instance method 'request_method' is defined in terms of this.

# File lib/fragmentary/fragment.rb, line 301
def request_method
  :get
end
request_options() click to toggle source

The instance method 'request_options' is defined in terms of this.

# File lib/fragmentary/fragment.rb, line 310
def request_options
  nil
end
request_parameters(*args) click to toggle source
# File lib/fragmentary/fragment.rb, line 305
def request_parameters(*args)
  nil
end
request_queues() click to toggle source
# File lib/fragmentary/fragment.rb, line 120
def request_queues
  @@request_queues ||= Hash.new do |hash, user_type|
    hash[user_type] = RequestQueue.new(user_type)
  end
  if self == base_class
    @@request_queues
  else
    return nil unless (requestable? or new.requestable?)
    user_types.each_with_object({}){|user_type, queues| queues[user_type] = @@request_queues[user_type]}
  end
end
requestable?() click to toggle source
# File lib/fragmentary/fragment.rb, line 296
def requestable?
  respond_to?(:request_path)
end
root(options) click to toggle source
# File lib/fragmentary/fragment.rb, line 52
def root(options)
  if fragment = options[:fragment]
    raise ArgumentError, "You passed Fragment #{fragment.id} to Fragment.root, but it's a child of Fragment #{fragment.parent_id}" if fragment.parent_id
  else
    klass, search_attributes, options = base_class.attributes(options)
    fragment = klass.where(search_attributes).includes(:children).first_or_initialize(options); fragment.save if fragment.new_record?
    fragment.set_indexed_children if fragment.child_search_key
  end
  fragment
end
set_record_type(type) click to toggle source

A subclass of a class declared with 'needs_record_id' will not have a record_type unless set explicitly, which can be done using the following method.

# File lib/fragmentary/fragment.rb, line 207
      def set_record_type(type)
        if needs_record_id?
          self.record_type = type
          if record_type_subscription = subscriber.subscriptions[record_type]
            # Set a callback on the eigenclass of an individual subscription to clean up client fragments
            # corresponding to a destroyed AR record. Note that this assumes that ALL fragments of a class
            # that calls this method should be removed if those fragments have a record_id matching the id
            # of the destroyed AR record. Also note that the call 'subscriber.subscriptions' above ensures that
            # the subscription exists even if the particular fragment subclass doesn't explicitly subscribe
            # to the record_type AR class. And note that if the fragment subclass does subscribe to the
            # record_type class, the callback doesn't affect the execution of any delete handler defined
            # by the fragment.
            class << record_type_subscription
              set_callback :after_destroy, :after, ->{subscriber.client.remove_fragments_for_record(record.id)}
            end
          end

          if requestable?
            record_class = record_type.constantize
            instance_eval <<-HEREDOC
              subscribe_to #{record_class} do
                def create_#{record_class.model_name.param_key}_successful(record)
                  request = Fragmentary::Request.new(request_method, request_path(record.id),
                                                     request_parameters(record.id), request_options)
                  queue_request(request)
                end
              end
            HEREDOC
          end

          define_method(:record){record_type.constantize.find(record_id)}
        end
      end
subscribe_to(publisher, &block) click to toggle source
# File lib/fragmentary/fragment.rb, line 282
def subscribe_to(publisher, &block)
  subscriber.subscribe_to(publisher, block)
end
subscriber() click to toggle source
# File lib/fragmentary/fragment.rb, line 136
def subscriber
  @subscriber ||= Subscriber.new(self)
end
touch_fragments_for_record(record_id) click to toggle source

Note that fragments matching the specified attributes won't always exist, e.g. if the page they are to appear on hasn't yet been requested, e.g. an assumption created on an article page won't necessarily have been rendered on the opinion analysis page.

# File lib/fragmentary/fragment.rb, line 274
def touch_fragments_for_record(record_id)
  fragments_for_record(record_id).includes({:parent => :parent}).each(&:touch)
end
user_type(user) click to toggle source

This default definition can be overridden by sub-classes as required (typically in root fragment classes by calling needs_user_type).

# File lib/fragmentary/fragment.rb, line 259
def user_type(user)
  user ? "signed_in" : "signed_out"
end
user_types() click to toggle source
# File lib/fragmentary/fragment.rb, line 253
def user_types
  ['signed_in']
end