Module: Fragmentary::Fragment::ClassMethods

Defined in:
lib/fragmentary/fragment.rb

Overview

Class Methods


Defined Under Namespace

Modules: RecordClassMethods

Instance Method Summary collapse

Instance Method Details

#acts_as_list_fragment(members:, list_record:, **options) ⇒ Object

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.


362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/fragmentary/fragment.rb', line 362

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) ⇒ Object

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).



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/fragmentary/fragment.rb', line 65

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 = {}

  if (parent_id = options.delete(:parent_id))
    search_attributes.merge!(:parent_id => parent_id)
  else
    application_root_url_column = Fragmentary.config.application_root_url_column
    if (application_root_url = options.delete(application_root_url_column)) && column_names.include?(application_root_url_column.to_s)
      search_attributes.merge!(application_root_url_column => application_root_url)
    end
  end

  [: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_storeObject



98
99
100
# File 'lib/fragmentary/fragment.rb', line 98

def cache_store
  @@cache_store ||= Rails.application.config.action_controller.cache_store
end

#child_search_keyObject



325
326
327
# File 'lib/fragmentary/fragment.rb', line 325

def child_search_key
  nil
end

#existing(options) ⇒ Object

ToDo: combine this with Fragment.root



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/fragmentary/fragment.rb', line 103

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_typeObject



120
121
122
# File 'lib/fragmentary/fragment.rb', line 120

def fragment_type
  self
end

#fragments_for_record(record_id) ⇒ Object



317
318
319
# File 'lib/fragmentary/fragment.rb', line 317

def fragments_for_record(record_id)
  self.where(:record_id => record_id)
end

#inherited(subclass) ⇒ Object

Subclass-specific request_queues



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/fragmentary/fragment.rb', line 143

def inherited(subclass)
  subclass.instance_eval do

    def request_queues
      super  # ensure that @@request_queues has been defined
      @request_queues ||= begin
        app_root_url = Fragmentary.application_root_url
        remote_urls = Fragmentary.config.remote_urls
        user_types.each_with_object( Hash.new {|hsh0, url| hsh0[url] = {}} ) do |user_type, hsh|
          # Internal request queues
          hsh[app_root_url][user_type] = @@request_queues[app_root_url][user_type]
          # External request queues
          if remote_urls.any?
            unless Rails.application.routes.default_url_options[:host]
              raise "Can't create external request queues without setting Rails.application.routes.default_url_options[:host]"
            end
            remote_urls.each {|remote_url| hsh[remote_url][user_type] = @@request_queues[remote_url][user_type]}
          end
        end
      end
    end

  end
  super
end

#key_nameObject



225
226
227
# File 'lib/fragmentary/fragment.rb', line 225

def key_name
  @key_name ||= nil
end

#list_record(association) ⇒ Object

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.



376
377
378
379
380
381
382
# File 'lib/fragmentary/fragment.rb', line 376

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) ⇒ Boolean

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


177
178
179
180
181
# File 'lib/fragmentary/fragment.rb', line 177

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 = {}) ⇒ Object



217
218
219
220
221
222
223
# File 'lib/fragmentary/fragment.rb', line 217

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?Boolean

Returns:

  • (Boolean)


306
307
308
# File 'lib/fragmentary/fragment.rb', line 306

def needs_key?
  false
end

#needs_record_id(options = {}) ⇒ Object

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).



235
236
237
238
239
240
# File 'lib/fragmentary/fragment.rb', line 235

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?Boolean

Returns:

  • (Boolean)


284
285
286
# File 'lib/fragmentary/fragment.rb', line 284

def needs_record_id?
  false
end

#needs_user_idObject

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.



188
189
190
# File 'lib/fragmentary/fragment.rb', line 188

def needs_user_id
  self.extend NeedsUserId
end

#needs_user_id?Boolean

Returns:

  • (Boolean)


288
289
290
# File 'lib/fragmentary/fragment.rb', line 288

def needs_user_id?
  false
end

#needs_user_type(options = {}) ⇒ Object

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.

For each class that declares ‘needs_user_type’, a set of user_types is defined that determines the set of request_queues that will be used to send requests to the application when a fragment is touched. By default these user_types are defined globally using ‘Fragmentary.setup’ but they can alternatively be set on a class-specific basis by passing a :session_users option to ‘needs_user_type’. See ‘Fragmentary.parse_session_users’ for details.



203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/fragmentary/fragment.rb', line 203

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?Boolean

Returns:

  • (Boolean)


302
303
304
# File 'lib/fragmentary/fragment.rb', line 302

def needs_user_type?
  false
end

#queue_request(request = nil) ⇒ Object



329
330
331
# File 'lib/fragmentary/fragment.rb', line 329

def queue_request(request=nil)
  request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request
end

#record_typeObject

Raises:

  • (ArgumentError)


242
243
244
245
# File 'lib/fragmentary/fragment.rb', line 242

def record_type
  raise ArgumentError, "The #{self.name} class has no record_type" unless @record_type
  @record_type
end

#remove_queued_request(user:, request_path:) ⇒ Object



169
170
171
# File 'lib/fragmentary/fragment.rb', line 169

def remove_queued_request(user:, request_path:)
  request_queues.each{|key, hsh| hsh[user_type(user)].remove_path(request_path)}
end

#requestObject

Raises:

  • (NotImplementedError)


351
352
353
# File 'lib/fragmentary/fragment.rb', line 351

def request
  raise NotImplementedError
end

#request_methodObject

The instance method ‘request_method’ is defined in terms of this.



338
339
340
# File 'lib/fragmentary/fragment.rb', line 338

def request_method
  :get
end

#request_optionsObject

The instance method ‘request_options’ is defined in terms of this.



347
348
349
# File 'lib/fragmentary/fragment.rb', line 347

def request_options
  {}
end

#request_parameters(*args) ⇒ Object



342
343
344
# File 'lib/fragmentary/fragment.rb', line 342

def request_parameters(*args)
  nil
end

#request_queuesObject

There is one queue per user_type per application instance (the current app and any external instances). The queues for all fragments are held in common by the Fragment base class here in @@request_queues but are also indexed on a subclass basis by an individual subclass’s user_types (see the inherited hook below). As well as being accessible here as Fragment.request_queues, the queues are also available without indexation as RequestQueue.all.



128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/fragmentary/fragment.rb', line 128

def request_queues
  @@request_queues ||= Hash.new do |hsh, host_url|
    # As well as acting as a hash key to index the set of request queues for a given target application instance
    # (for which its uniqueness is the only requirement), host_url is also passed to the RequestQueue constructor,
    # from which it is used:
    #   (i) by the RequestQueue::Sender to derive the name of the delayed_job queue that will be used to process the
    #       queued requests if the sender is invoked in asynchronous mode - see RequestQueue::Sender#schedulerequests.
    #   (ii) by the Fragmentary::InternalUserSession instantiated by the Sender to configure the session_host.
    hsh[host_url] = Hash.new do |hsh2, user_type|
      hsh2[user_type] = RequestQueue.new(user_type, host_url)
    end
  end
end

#requestable?Boolean

Returns:

  • (Boolean)


333
334
335
# File 'lib/fragmentary/fragment.rb', line 333

def requestable?
  respond_to?(:request_path)
end

#root(options) ⇒ Object



50
51
52
53
54
55
56
57
58
59
# File 'lib/fragmentary/fragment.rb', line 50

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) ⇒ Object

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.



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/fragmentary/fragment.rb', line 249

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)}
        set_callback :after_create, :after, ->{subscriber.client.try_request_for_record(record.id)}
      end
    end

    self.extend RecordClassMethods
    define_method(:record){record_type.constantize.find(record_id)}
  end
end

#subscribe_to(publisher, &block) ⇒ Object



321
322
323
# File 'lib/fragmentary/fragment.rb', line 321

def subscribe_to(publisher, &block)
  subscriber.subscribe_to(publisher, block)
end

#subscriberObject



173
174
175
# File 'lib/fragmentary/fragment.rb', line 173

def subscriber
  @subscriber ||= Subscriber.new(self)
end

#touch_fragments_for_record(record_id) ⇒ Object

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.



313
314
315
# File 'lib/fragmentary/fragment.rb', line 313

def touch_fragments_for_record(record_id)
  fragments_for_record(record_id).includes({:parent => :parent}).each(&:touch)
end

#user_type(user) ⇒ Object

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



298
299
300
# File 'lib/fragmentary/fragment.rb', line 298

def user_type(user)
  user ? "signed_in" : "signed_out"
end

#user_typesObject



292
293
294
# File 'lib/fragmentary/fragment.rb', line 292

def user_types
  ['signed_in']
end