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.


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
416
417
418
419
420
421
# File 'lib/fragmentary/fragment.rb', line 368

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



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
97
# File 'lib/fragmentary/fragment.rb', line 66

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



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

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

#child_search_keyObject



331
332
333
# File 'lib/fragmentary/fragment.rb', line 331

def child_search_key
  nil
end

#existing(options) ⇒ Object

ToDo: combine this with Fragment.root



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

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



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

def fragment_type
  self
end

#fragments_for_record(record_id) ⇒ Object



323
324
325
# File 'lib/fragmentary/fragment.rb', line 323

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

#inherited(subclass) ⇒ Object

Subclass-specific request_queues



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

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



231
232
233
# File 'lib/fragmentary/fragment.rb', line 231

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.



382
383
384
385
386
387
388
# File 'lib/fragmentary/fragment.rb', line 382

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)


183
184
185
186
187
# File 'lib/fragmentary/fragment.rb', line 183

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



223
224
225
226
227
228
229
# File 'lib/fragmentary/fragment.rb', line 223

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)


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

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



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

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)


290
291
292
# File 'lib/fragmentary/fragment.rb', line 290

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.



194
195
196
# File 'lib/fragmentary/fragment.rb', line 194

def needs_user_id
  self.extend NeedsUserId
end

#needs_user_id?Boolean

Returns:

  • (Boolean)


294
295
296
# File 'lib/fragmentary/fragment.rb', line 294

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.



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/fragmentary/fragment.rb', line 209

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.user_types
    end
  end
end

#needs_user_type?Boolean

Returns:

  • (Boolean)


308
309
310
# File 'lib/fragmentary/fragment.rb', line 308

def needs_user_type?
  false
end

#queue_request(request = nil) ⇒ Object



335
336
337
# File 'lib/fragmentary/fragment.rb', line 335

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

#record_typeObject

Raises:

  • (ArgumentError)


248
249
250
251
# File 'lib/fragmentary/fragment.rb', line 248

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

#remove_queued_request(host_url: nil, user:, request_path:) ⇒ Object



170
171
172
173
174
175
176
177
# File 'lib/fragmentary/fragment.rb', line 170

def remove_queued_request(host_url: nil, user:, request_path:)
  u_type = user_type(user)
  if host_url.is_a?(String) and (queue = request_queues[host_url][u_type])
    queue.remove_path(request_path)
  else
    request_queues.each{|key, hsh| hsh[u_type].remove_path(request_path)}
  end
end

#requestObject

Raises:

  • (NotImplementedError)


357
358
359
# File 'lib/fragmentary/fragment.rb', line 357

def request
  raise NotImplementedError
end

#request_methodObject

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



344
345
346
# File 'lib/fragmentary/fragment.rb', line 344

def request_method
  :get
end

#request_optionsObject

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



353
354
355
# File 'lib/fragmentary/fragment.rb', line 353

def request_options
  {}
end

#request_parameters(*args) ⇒ Object



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

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.



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

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)


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

def requestable?
  respond_to?(:request_path)
end

#root(options) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
# 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.



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/fragmentary/fragment.rb', line 255

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



327
328
329
# File 'lib/fragmentary/fragment.rb', line 327

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

#subscriberObject



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

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.



319
320
321
# File 'lib/fragmentary/fragment.rb', line 319

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



304
305
306
# File 'lib/fragmentary/fragment.rb', line 304

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

#user_typesObject



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

def user_types
  ['signed_in']
end