fragmentary 0.2.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 2747ef223962cc8eaa11b9d929febf77a5746bca
4
- data.tar.gz: 8eb6ca9f3d487102cf23bb8944f46a856230371c
2
+ SHA256:
3
+ metadata.gz: 61a94b394035bb4b3ec317316e46fa77ca925cc15a162a76d6401500e6636b5d
4
+ data.tar.gz: 8db2f91e9a9b23f3ccdafbc2f800695e653426a656887959f807c80eca10c901
5
5
  SHA512:
6
- metadata.gz: dcf5246ca4dd67f075f74b7d951e6e69d57dce8269ed49e6896cb92d45e2079500317345e95bb63208d7035e9988f6c03ff653eab2d88c410bfe6de3a9509b99
7
- data.tar.gz: f0498b8f900c03dc05d0469fbf5cde755b91b7a941da85cfc9394c5d730cc40ab9f097a4e2e1c38bc73e62700cc0bf83d0360233fb6e5d580dd9984b45b2a527
6
+ metadata.gz: bc4f5da40872cfb5327af28cb81862e61ba201258bc8338fa6ad9871439dac0133af4c102c10b09cd0ed25acb883c53d428ba3632085ac4b67cb7f662ccf6c16
7
+ data.tar.gz: '0628b394849e14ed4fed5ebe18e7b59a4badfe83d16b63f24965e6e6a2a4d656aec57f627000d70286c79152d2d028fb231c0b7c89a2eabfffb455140f7404e6'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ### 0.4.0
2
+ - Uses ActiveJob as the interface for asynchronous processes instead of using Delayed::Job directly.
3
+ - Updates Rails dependency to ~>6.0 as earlier versions lack support for the custom job serialization needed to use ActiveJob. Accordingly, Rails 5.x and earlier are no longer supported.
4
+ - Adds a convenience method RequestQueue.send_all for sending internal application requests.
5
+ - Inserts optional HTML comments containing time and version stamps into fragment content.
6
+ - Adds utility methods for interacting with the cache_store from the console.
7
+ - Updates documentation for new features and to reflect Rails use of cache versioning.
8
+
9
+ ### 0.3.0
10
+ - Updates gem to support Rails 5.x (Rails 4.x and earlier are no longer supported due to Rails API changes).
11
+ - Adds support for multiple application instances, allowing pre-release application code to be staged for testing.
12
+ - Fixes a bug in Fragment.set_record_type affecting some application data handlers for fragment classes that are subclassed from another.
13
+
1
14
  ### 0.2.2
2
15
  - Removes validation of the fragment's root_id (only relevant to child fragments); ignore v0.2.1
3
16
 
data/README.md CHANGED
@@ -5,16 +5,21 @@ Fragmentary augments the fragment caching capabilities of Ruby on Rails to suppo
5
5
  * multiple versions of individual fragments for different groups of users, e.g. admin vs regular users
6
6
  * post-cache insertion of user-specific content
7
7
  * automatic refreshing of cached content when application data changes, without an external client request
8
+ * multiple application instances running concurrently with shared application data
8
9
 
9
- **Note**: Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use. See [Integration Issues](https://github.com/MarkMT/fragmentary/blob/master/README.md#integration-issues) for details of issues that should be considered when using it elsewhere.
10
+ Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use.
10
11
 
11
12
  ## Background
12
- In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. The content is stored in the cache with a key value derived from the `updated_at` attribute of that record. If any attributes of the record change, the cached entry automatically expires and on the next browser request for that content the fragment is re-rendered using the current data. In the view, the `cache` helper is used to specify the record used to determine the key and define the content to be rendered within the fragment, e.g.:
13
+ In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. In the view, the `cache` helper is used to specify the record and to define the content to be rendered within the fragment, e.g.:
13
14
  ```
14
15
  <% cache product do %>
15
16
  <%= render product %>
16
17
  <% end %>
17
18
  ```
19
+ The content is stored in the cache using a _key_ value derived from the record's `id` attribute. The key identifies where the cached content is stored. In addition, a _version_ value derived from the record's `updated_at` attribute is stored along with the content [^1]. If any attributes of the record change, the `updated_at` attribute also changes; the next time a browser requests the content, a mismatch will exist between the calculated and stored values of the version, causing the cache to expire and the fragment to be re-rendered using the current data.
20
+
21
+ [^1]: Prior to Rails 5.2, the value of `updated_at` was incorporated into the cache key rather that the version. That behavior is still available by setting `config.active_record.cache_versioning = false` in `config/application.rb`.
22
+
18
23
  For data models with a `has_one` or `has_many` association, nested (Russian Doll) caching is possible with the inner fragment representing the associated record. For example if a product has many games -
19
24
  ```
20
25
  <% cache product do %>
@@ -34,14 +39,14 @@ end
34
39
 
35
40
  Part of the beauty of this approach is the fact that any change in data requiring the cache to be refreshed is detected by inspecting just a single `updated_at` attribute of each record. However it is less effective at handling cases where the content of a fragment depends in more complex ways on multiple records from multiple models. The fact that automatic updating of nested fragments is limited to `belongs_to` associations is one aspect of this.
36
41
 
37
- It is certainly possible to construct more complex keys from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
42
+ It is certainly possible to construct more complex keys and/or versions from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
38
43
 
39
- A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, but we are still left with the challenge of how to implement nesting in the case of more complex data associations.
44
+ A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, though we are still left with the challenge of how to implement nesting in the case of more complex data associations.
40
45
 
41
46
  ## Fragmentary - General Approach
42
- Fragmentary uses a database table and corresponding ActiveRecord model that are separate from your application data specifically to represent view fragments. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the cache key is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
47
+ In Fragmentary, view fragments are associated with records from an ActiveRecord model and corresponding database table that are _separate_ from your application data. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the version of a cache entry is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
43
48
 
44
- To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that the whole page (or part thereof in the case of some AJAX requests) is refreshed.
49
+ To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that all of the cached content containing the change is refreshed.
45
50
 
46
51
  Rather than use Rails' native `cache` helper directly, Fragmentary provides some new helper methods that hide some of the necessary internals involved in working with an explicit fragment model. Additional features include the ability to cache separate versions of fragments for different types of users and the ability to insert user-specific content after cached content is retrieved from the cache store. Fragmentary also supports automatic background updating of cached content within seconds of application data changing, avoiding the need for a user to visit a page in order to update the cache.
47
52
 
@@ -116,15 +121,30 @@ class ProductTemplate < Fragment
116
121
  needs_record_id :type => 'Product'
117
122
  end
118
123
  ```
119
- Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record. If you need to define further subclasses of your initial subclass, you can if necessary declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
124
+ Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record and the `record_id` attribute of a `ProductTemplate` record contains the `id` of that product.
125
+
126
+ If you need to define further subclasses of your initial subclass, you can declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
120
127
 
121
128
  `set_record_type 'SomeModelName'`
122
129
 
123
- We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics but are used in different contexts to represent different kinds of content.
130
+ We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics in common but are used in different contexts to represent content with different sub-class-specific dependencies on application data.
124
131
 
125
- Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block containing method definitions to handle create, update and destroy events on your application data, typically to touch the fragment records affected by the application data change. The names of these methods follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
132
+ Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block to define methods that handle create, update and destroy events on your application data, typically to touch the fragment records affected by a change in the application data. For example, if product listings include the names of all the categories the products belong to, and those categories are represented by a separate ActiveRecord model, if the wording of a category name changes, you could handle that as follows:
133
+ ```
134
+ class ProductTemplate < Fragment
135
+ needs_record_id :type => 'Product'
126
136
 
127
- Within the body of each method you define within the `subscribe_to` block, you can retrieve and touch the fragment records affected by the change in application data. The method `touch_fragments_for_record` can be used for convenience. That method takes an individual application data record or record_id or an array of either. So, for example, if product listings include the names of all the categories the products belong to, with those categories being represented by a separate ActiveRecord model, and the wording of a category name changes, you could handle that as follows.
137
+ subscribe_to 'ProductCategory' do
138
+ def update_product_category_successful(product_category)
139
+ # touch ProductTemplate records whose record_id matches the ids of products within the specified product_category
140
+ ProductTemplate.where(:record_id => product_category.products.map(&:id)).each(&:touch)
141
+ end
142
+ end
143
+ end
144
+ ```
145
+ The effect of this will be to expire the product template fragment for every product contained within the affected category. `subscribe_to` takes one parameter, the name of the application data class being handled, while the names of the methods defined within the block follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
146
+
147
+ Within the body of each method that you define within the block, the convenience method `touch_fragments_for_record` can be used retrieve and touch the fragment records affected by the change in application data. This method takes an individual application data record or a record_id or an array of either. So the example above can be written instead as:
128
148
  ```
129
149
  class ProductTemplate < Fragment
130
150
  needs_record_id :type => 'Product'
@@ -136,9 +156,7 @@ class ProductTemplate < Fragment
136
156
  end
137
157
  end
138
158
  ```
139
- The effect of this will be to expire the product template fragment for every product contained within the affected category.
140
-
141
- When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining (so the methods we define within the block are actually defined on that subscriber object). However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
159
+ When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining. So the methods we define within the block are actually defined on that subscriber object. However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the associated fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
142
160
 
143
161
  Note also that for fragment subclasses that declare `needs_record_id`, there is no need to define a `destroy_<model_name>_successful` method simply to remove a fragment whose `record_id` matches the `id` of a <model_name> application record that is destroyed, e.g. to destroy a `ProductTemplate` whose `record_id` matches the `id` of a destroyed `Product` object. Fragmentary handles this clean-up automatically. This is not to say, however, that 'destroy' handlers are never needed at all. The destruction of an application data record will often require other fragments to be touched.
144
162
 
@@ -157,7 +175,7 @@ A 'root' fragment is one that has no parent. In the template in which a root fra
157
175
 
158
176
  #### Nested Fragments
159
177
 
160
- The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` it takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
178
+ The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template's execution context (i.e. the receiver of the `cache_fragment` method) as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
161
179
  ```
162
180
  <% cache_fragment :type => 'ProductTemplate', :record_id => @product.id do |fragment| %>
163
181
  <% fragment.cache_child :type => 'StoresAvailable' do |child_fragment| %>
@@ -186,9 +204,11 @@ class StoresAvailable < Fragment
186
204
  acts_as_list_fragment :members => :product_stores, :list_record => :product
187
205
  end
188
206
  ```
207
+ The effect of this declaration is to ensure that whenever a new `ProductStore` is created, the `StoresAvailable` record for the corresponding product will be touched, so that the next time the list is rendered, the required new `AvailableStore` fragment will be created.
208
+
189
209
  `acts_as_list_fragment` takes two named arguments:
190
- * `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. The effect of declaring `acts_as_list_fragment` is to ensure that when that membership association is created, the list fragment it is being added to is touched, expiring the cache so that on the next request the list will be re-rendered, which has the effect of creating the required new `AvailableStore` fragment. Note that the value of the `members` argument should match the record type of the list item fragments. So in the example, the `AvailableStore` class should be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implied redundancy here that could be problematic; some adjustment may be made in the future).
191
- * `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the record_id (or the record itself; either will work) associated with the list fragment from a given membership association. If the value is a method name, the list record is found by calling that method on the membership association. In the example, the membership association is a `ProductStore` record, say `product_store`. The list is represented by a `StoresAvailable` fragment whose `record_id` points to a `Product` record. We can get that `Product` record simply by calling `product_store.product`, so the `list_record` parameter passed to `acts_as_list_fragment` is just the method `:product` (`:product_id` would also work). However sometimes a simple method like this is insufficient and a `Proc` may be used instead. In this case the newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
210
+ * `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type is what triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. Note that the value of the `members` argument should match the record type of the list item fragments (i.e. the type provided to `needs_record_id` in the definition of the list item fragment class). So in the example, the `AvailableStore` class would be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implicit redundancy here that could be problematic; some adjustment may be made in the future).
211
+ * `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the `record_id` (or the record itself; either will work) associated with the list fragment from a given membership association. So, in the example, the membership association is a `ProductStore` record, say `product_store`, and `:product` is the method that would be called on `product_store` to produce the product whose `id` is the `record_id` of the `StoresAvailable` list fragment that needs to be touched when a new list item is created (`:product_id` would also work). Sometimes, however, a simple method like this is insufficient and a `Proc` may be used instead. The newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
192
212
 
193
213
  Note that in the example, the specified `list_record` method, `:product`, returns a single record for a given `product_store` membership association. However this isn't necessarily always the case. The method or `Proc` may also return an array of records or record_ids.
194
214
 
@@ -218,7 +238,7 @@ then creating an initial product_store association won't add the initial item si
218
238
  <% end %>
219
239
  ```
220
240
 
221
- It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example this would look like the following.
241
+ It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example, this would look like the following.
222
242
  ```
223
243
  class StoresAvailable < Fragment
224
244
  subscribe_to 'ProductStore' do
@@ -229,13 +249,13 @@ class StoresAvailable < Fragment
229
249
  end
230
250
  ```
231
251
 
232
- In fact there can be cases where it is actually necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
252
+ In fact there can be cases where it is necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
233
253
 
234
254
  ### Accessing Fragments by Arbitrary Application Attributes
235
255
 
236
- The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the fragment type, the `parent_id` if the fragment is nested, and the `record_id`.
256
+ The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the `record_id`, the fragment type, and the `parent_id` if the fragment is nested.
237
257
 
238
- Of course it is not always necessary to have a `record_id`, for example in the case of an index page. On the other hand, there are also cases where something other than a `record_id` is needed to uniquely identify a fragment. Suppose, for example, you have a fragment that renders a set of books published in a certain year, a set of restaurants in a certain postcode, or a set of individuals (say sporting event competitors) in a particular age-group category. In cases like these we need to be able to uniquely identify the fragment again by its type and its `parent_id` (if it is nested), but also by some other parameter, which we refer to as a 'key' (the terminology may be a little unfortunate; this is not the same thing as a _cache key_).
258
+ Of course, it is not always necessary to have a `record_id`, for example in a fragment representing a list of items on an index page. On the other hand, there are also cases where something other than a `record_id` is needed in order to uniquely identify a fragment. Suppose, for example, you have a fragment that renders a set of books published in a certain year, a set of restaurants in a certain postcode, or a set of competitors in a sporting event within a particular age-group category. In cases like these we need to be able to uniquely identify the fragment again by its type and sometimes its `parent_id` (if it is nested), but also by some other parameter, which we refer to as a 'key' (the terminology may be a little unfortunate; this is not the same thing as a _cache key_).
239
259
 
240
260
  To facilitate this, the fragment model includes a `key` attribute that can be customized on a per-class basis using the `needs_key` method. For example,
241
261
  ```
@@ -252,7 +272,7 @@ With this definition we can define the cached content like this:
252
272
  <% end %>
253
273
  <% end %>
254
274
  ```
255
- Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could for example write `fragment.year_of_publication`.
275
+ Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` (`year` in the example above) can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could in this example write `fragment.year_of_publication`.
256
276
 
257
277
  ### User Specific Content
258
278
 
@@ -488,9 +508,9 @@ When application data that a cached fragment depends upon changes, i.e. as a res
488
508
 
489
509
  As part of this process, new fragment records may be created as *children* of an existing fragment if the existing fragment contains newly added list items. A new *root* fragment, on the other hand, will be created for any root fragment class defined with `needs_record_id` if an application data record of the associated `record_type` is created, as soon as a browser request is made for the new content (often a new page) containing that fragment.
490
510
 
491
- Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur.
511
+ Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur. These 'internal' requests are sent asynchronously so that they do not delay the response to the external POST, PATCH or DELETE request from the user's browser that brought about the change in application data.
492
512
 
493
- Creating these requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (in general, the request can be suppressed by passing `:no_request => true` if required). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
513
+ Creating internal requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (If required, the request can be suppressed by passing `:no_request => true` to `touch`). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
494
514
  ```
495
515
  class ProductTemplate < Fragment
496
516
  needs_record_id :type => 'Product'
@@ -522,15 +542,15 @@ So in this example, any time a new `Product` record is created, Fragmentary will
522
542
 
523
543
  #### Request Queues
524
544
 
525
- A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, considering that different versions of some cached fragments exist for different user types, in order to ensure that all affected versions get refreshed we may need to send each internal request multiple times in the context of several different user sessions, each representing a different user type.
545
+ A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, given that different versions of some cached fragments may exist for different user types, we may need to send each internal request multiple times in the context of several different user sessions (representing different user types), to ensure that all affected versions get refreshed.
526
546
 
527
- To achieve this, during handling of the initial external request in which changes in application data propagate to affected fragment records via the `subscribe_to` declarations in your fragment class definitions, multiple instances of class `Fragmentary::RequestQueue`, each corresponding to a different user type, are used to store a collection of `Fragmentary::Request` objects representing the internal requests generated during that process.
547
+ To achieve this, during the handling of the initial external request that causes the application data to change, the internal requests created as a result are represented by instances of the class `Fragmentary::Request` and are initially stored in a number of 'queues' represented by instances of the class `Fragmentary::RequestQueue`. Separate queues exist for each of the different user types supported by the fragments to be rendered, and individual requests to a given endpoint are automatically placed in multiple queues as needed. However, responsibility for actually sending the requests stored in the queues is offloaded to an asynchronous process using Rails' `ActiveJob` interface (as detailed below under [Sending Queued Requests](https://github.com/MarkMT/fragmentary#sending-queued-requests)).
528
548
 
529
- The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user).
549
+ `Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports. So, for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`.
530
550
 
531
- Configuration of the `user_types` method is discussed in the next section. Fragmentary uses the types returned by this method to identify the request queues that each internal application request needs to be added to when instances of each particular `Fragment` subclass are updated. `Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports.
551
+ The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user). Configuration of the `user_types` method is discussed in the next section.
532
552
 
533
- The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`. If a `StoreTemplate` fragment has one request queue `StoreTemplate.request_queues["signed_in"]`, the `"signed_in"` queues for both classes represent the same object.
553
+ The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example,`ProductTemplate.request_queues["signed_in"]` and `StoreTemplate.request_queues["signed_in"]` both represent the same object.
534
554
 
535
555
  #### Configuring Internal Request Users
536
556
 
@@ -568,16 +588,34 @@ The value of the `:session_users` option (you can also use `:user_types` or just
568
588
 
569
589
  #### Sending Queued Requests
570
590
 
571
- A class method `Fragmentary::RequestQueue.all` returns an array of all request queues the application uses. The requests stored within a given `RequestQueue` can be sent to the application by calling the instance method `RequestQueue#send`. Calling `send` instantiates a `Fragmentary::UserSession` object representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` will sign in to the application using the credentials configured for the particular `user_type`.
591
+ Internal requests stored in the application's request queues are typically sent using an `after_action` filter in your `ApplicationController`, e.g. for create, update and destroy actions:
592
+ ```
593
+ class ApplicationController
594
+ after_action :send_queued_requests, :only => [:create, :update, :destroy]
595
+
596
+ def send_queued_requests
597
+ Fragmentary::RequestQueue.send_all(:between => 10.seconds)
598
+ end
599
+ end
600
+ ```
601
+ The optional named argument `between` represents the interval between individual requests in each queue being sent and allows you to throttle the request rate. If it is present (including `:between => 0.seconds`), the requests will be sent asynchronously using ActiveJob. If omitted, requests will be sent immediately (i.e before the response to the original external request is sent to the user's browser).
572
602
 
573
- To send all requests once processing of each external browser request has been completed, add a method such as the following to your `ApplicationController` class and call it using a controller `after_filter`, e.g. for create, update and destroy actions:
603
+ You can customize further by sending requests from queues individually. A class method `Fragmentary::RequestQueue.all` returns an array of all request queues that the application uses. The requests stored within an individual queue can be sent by calling the instance method `RequestQueue#send`.
574
604
  ```
575
605
  def send_queued_requests
576
606
  delay = 0.seconds
577
607
  Fragmentary::RequestQueue.all.each{|q| q.send(:delay => delay += 10.seconds)}
578
608
  end
579
609
  ```
580
- The `send` method takes two optional named arguments, `delay` and `between`. If neither are present, all requests held in the queue are sent immediately. If either are present, sending of requests is off-loaded to an asynchronous process using the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) and scheduled according to the parameters provided: `delay` represents the delay before the queue begins sending requests and `between` represents the interval between individual requests in the queue being sent. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. You may customize as appropriate.
610
+ `send` takes two optional named arguments, `delay` and `between`. `delay` represents the delay before the queue begins sending requests and `between` again represents the interval between individual requests. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. If either are present (including `:delay => 0`), the requests will be sent asynchronously using ActiveJob. If omitted, all requests held in the queue are sent immediately.
611
+
612
+ When requests are sent using either `send` or `send_all`, a `Fragmentary::UserSession` object is instantiated representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` automatically signs in to the application using the credentials configured for the particular `user_type`.
613
+
614
+ ActiveJob supports a number of adapters for executing tasks asynchronously. You should configure according to your own needs. For example, to use Fragmentary with the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem, you would add the following to your `config/application.rb` file:
615
+ ```
616
+ config.active_job.queue_adapter = :delayed_job
617
+ ```
618
+ and add the 'delayed_job_active_record' gem to your Gemfile. Consult the [ActiveJob documentation](https://guides.rubyonrails.org/active_job_basics.html#backends) for more information as well as the documentation for your chosen adapter on how to start background task processes when you deploy your application.
581
619
 
582
620
  #### Queuing Requests Explicitly
583
621
 
@@ -627,31 +665,20 @@ class AvailableStore < Fragment
627
665
  end
628
666
  end
629
667
  ```
630
- While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process, which we again (currently) do using Delayed::Job.
668
+ While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process. We again do this using ActiveJob in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
631
669
 
632
- To facilitate this we use the `Fragmentary::Dispatcher` class. A dispatcher object is instantiated with an array containing all existing handlers, retrieved using the class method `Fragmentary::Handler.all`.
633
670
  ```
634
- dispatcher = Fragmentary::Dispatcher.new(Fragmentary::Handler.all)
635
- ```
636
- The `Dispatcher` class defines an instance method `perform`, which invokes `call` on all the handlers provided when the dispatcher is instantiated. Delayed::Job uses the `perform` method to define the job to be run asynchronously, which is accomplished by passing the dispatcher object to `Delayed::Job.enqueue`:
637
- ```
638
- Delayed::Job.enqueue(dispatcher)
671
+ def handle_asynchronous_tasks
672
+ send_queued_requests # as defined earlier
673
+ Fragmentary::DispatchHandlersJob.perform_later(Fragmentary::Handler.all)
674
+ Fragmentary::Handler.clear
675
+ end
639
676
  ```
640
- As in the case of our queued background requests, we enqueue the dispatcher in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
677
+ Here `Fragmentary::DispatchHandlersJob` is a subclass of `ActiveJob::Base`, which means if you wish you can specify additional options using the `set` method, e.g.
641
678
  ```
642
- class ApplicationController < ActionController::Base
643
-
644
- after_filter :handle_asynchronous_tasks
645
-
646
- def handle_asynchronous_tasks
647
- send_queued_requests # as defined earlier
648
- Delayed::Job.enqueue(Fragmentary::Dispatcher.new(Fragmentary::Handler.all))
649
- Fragmentary::Handler.clear
650
- end
651
-
652
- end
679
+ Fragmentary::DispatchHandlersJob.set(:wait => 1.minute, :queue => 'low_priority').perform_later(Fragmentary::Handler.all)
653
680
  ```
654
- Note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. The `Dispatcher` takes care of sending these additional requests.
681
+ Also note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. `DispatchHandlersJob` also takes care of sending these additional requests.
655
682
 
656
683
  #### Updating Lists Asynchronously
657
684
 
@@ -758,21 +785,113 @@ Note that if the partial page content being generated contains several nested ch
758
785
 
759
786
  It is possible to define a fragment without actually storing its content in the cache store. This can be useful, for example if you wish to cache several sibling children within a page but don't need to store the entire root fragment that contains them. Simply include the option `:no_cache => true` in the hash passed to `cache_fragment` or `cache_child`.
760
787
 
761
- ## Integration Issues
788
+ ### Support for Multiple Application Instances
762
789
 
763
- There are some aspects of this pre-release version of Fragmentary that reflect the application context in which it was originally developed and may need adjustment before deployment elsewhere. Note in particular the following:
764
- 1. Fragmentary was created in the context of a Rails 4.x application (for perfectly sound reasons! :)). There should be only minor adjustment required for use within a Rails 5.x application, but two specific issues we are aware of are the following:
765
- - Rails 5.x changes the API for `ActionDispatch::Integration::Session` and now requires that HTTP request parameters be passed as a named parameter `:params`, rather than an unnamed hash in Rails 4.x. This affects the method `to_proc` in class `Fragmentary::Request` and the methods `sign_in` and `sign_out` in class `Fragmentary::UserSession`.
766
- - In 'lib/fragmentary/fragment.rb', we set `cache_timestamp_format = :usec` to overcome a timestamp resolution problem when using caching with Postgres under Rails 4.x. We believe that this problem has been solved in Rails 5.x, so this setting will not be necessary. See https://github.com/rails/rails/issues/21815.
767
- 1. Fragmentary uses the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) to execute background tasks asynchronously. Other alternatives exist within the Rails ecosystem, and in Rails 5.x it will probably make sense to use [Active Job](https://guides.rubyonrails.org/active_job_basics.html) as an abstraction layer.
790
+ In many practical deployment scenarios it is desirable to maintain a live pre-release version of the application online separate from the public-facing production website. This allows new software releases to be staged for either internal or beta testing prior to final deployment to the production environment. For example, if the public-facing application is accessed at a root URL of http://myapp.com/, a separate pre-release version might be deployed say to http://prerelease.myapp.com/ or http://myapp.com/prerelease/.
768
791
 
769
- ## Contributing
792
+ In the specific scenario in which Fragmentary was developed, it was important for the pre-release version of the application to share the same application database as the production site, i.e. both production and pre-release versions render exactly the same data, and any changes to application data initiated by a user of one version of the application will be reflected in the content seen by a user of the other version as well. For caching, this leads to some additional challenges which we discuss below.
770
793
 
771
- Bug reports and usage questions are welcome at https://github.com/MarkMT/fragmentary.
794
+ #### Storing Multiple Versions of Content in the Cache
795
+
796
+ There is an important difference between the content rendered by each instance of an application: any HTML links to other pages on the site must be to URLs representing the particular version being rendered. For example, on a 'products' index page, a link to an individual product page on the production website might look like http://myapp.com/products/123, while the same link on the index page of the pre-release version might be http://myapp.com/prerelease/products/123.
797
+
798
+ In general, as long as links contained in the view are created using Rails' standard path or url helpers, e.g. `product_path(@product)` etc, they will automatically be based on the root URL of the application instance (production or pre-release) in which they are generated. However, in the context of caching, the fact that differences exist in the content generated by different application instances implies that for each fragment defined in the view by calling `cache_fragment` or `cache_child`, different versions of fragment content need to be stored in the cache. Also, each of these distinct versions needs to be associated with a unique record in the `fragments` database table identifying which application instance generated that content.
799
+
800
+ In Fragmentary we can accomplish this simply by adding a column to the `fragments` table to store the root URL of the particular application instance that created the fragment content.
801
+
802
+ ```
803
+ class AddAppUrlToFragment < ActiveRecord::Migration
804
+ def change
805
+ change_table :fragments do |t|
806
+ t.string :application_root_url
807
+ end
808
+ end
809
+ end
810
+ ```
811
+
812
+ The column name `application_root_url` shown above is the default assumed by Fragmentary. You can use a different column name if you wish, as long as you tell Fragmentary in initializers/fragmentary.rb, e.g.:
813
+
814
+ ```
815
+ Fragmentary.setup do |config|
816
+ ...
817
+ config.application_root_url_column = 'app_root'
818
+ end
819
+ ```
820
+
821
+ Once this column has been added to the `fragments` table, any fragment records subsequently created by calls to `cache_fragment` or `cache_child` will have the column populated automatically based on the particular application instance in which the code is executed, and the content stored in the cache for each fragment record will be that generated by the corresponding instance. As long as any links rendered within the content are generated using Rails' path or url helpers, the cached content will be rendered correctly both when initially created and when retrieved subsequently.
822
+
823
+ Note that it is possible to store cached content for each of the different application instances in different places. Simply set `config.cache_store` in config/environments/production.rb (or alternative environment file) as required.
824
+
825
+ #### Automatically Refreshing Multiple Versions of Cached Content
826
+
827
+ The fact that two versions of content exist in the cache for each fragment means that whenever a change in application data occurs that triggers the generation of an internal application request to refresh a piece of cached content (i.e. for any fragment that has a `request_path` method defined), _both_ (in general all) versions need to be refreshed. So for example, a change to application data caused by a user action on the production website needs to trigger internal requests to _both_ the production _and_ the pre-release instances in order to refresh their respective content. The converse is true for changes initiated from the pre-release website.
828
+
829
+ (Note: for better or worse, we've stuck with the term 'internal request' to mean any request delivered to the application programmatically in order to refresh cached content. This includes requests created by one application instance that are intended to be processed by another instance.)
830
+
831
+ Our approach to sending requests between application instances relies on processing requests asynchronously and directing the tasks used to do this to specific task queues associated with the application instance they are intended to be processed by. Each application instance has an associated asynchronous task process (in our case using Delayed::Job). By configuring that process to run tasks from just the queue(s) designated for it, we ensure that any application instance can direct requests to any other. As long as queued tasks are stored in a database table (as they are, for example, using Delayed::Job) and provided that each instance of the application has access to the same database, this approach achieves our objective. The way we accomplish this in practice is outlined below.
832
+
833
+ First, to configure Fragmentary to automatically refresh cached content for multiple instances, set `remote_urls` in `Fragmentary.config`. This is an array of root URLs for all _other_ instances of the application that requests should be sent to. For example, in order to allow requests to be sent from the production instance to the pre-release instance, in initializers/fragmentary.rb in the production code we would add the following configuration:
834
+
835
+ ```
836
+ Fragmentary.setup do |config|
837
+ ...
838
+ config.remote_urls << 'http://myapp.com/prerelease/'
839
+ end
840
+ ```
841
+
842
+ Our current approach to application deployment is to maintain different branches in our source repository for each application instance. This allows us to keep custom configurations like `config.remote_urls` above for each instance on their own branches. So in contrast to the case above, to allow requests to be sent from the pre-release instance to the production instance, in initializers/fragmentary.rb on the pre-release branch the `remote_urls` array would be set to `['http://myapp.com/']` .
843
+
844
+ As an alternative, it would also be possible to create a separate environment for the pre-release deployment in your config/environments directory and thus maintain the configuration for all application instances in a single repository branch. We have not investigated this approach.
845
+
846
+ By specifying `remote_urls` as described above, for each internal application request Fragmentary will automatically create tasks to process the request not only on the application instance where it was created but also on all other instances corresponding to the elements in `remote_urls`. Fragmentary sends these tasks to named queues whose names are formed automatically from the domain name and path of both the root URL of the current instance and the values in `remote_urls`. So in the example, a request to '/products/123', created say by editing a product name on the production website, would be sent to queues with names 'myapp.com/' and 'myapp.com/prerelease/'.
847
+
848
+ The final step is to configure the asynchronous processes associated with each application instance to execute tasks from the correct queue. In our case, we use [Delayed::Job](https://github.com/collectiveidea/delayed_job) to handle asynchronous tasks and Capistrano 2 for deployment. We set `delayed_job_args` in `config/deploy.rb` to configure the queue names as follows (see documentation [here](https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/recipes.rb)):
849
+ ```
850
+ queue_prefix = "myapp.com/prerelease/"
851
+ set :delayed_job_args, "--queue=#{queue_prefix}"
852
+ after "deploy:stop", "delayed_job:stop"
853
+ after "deploy:start", "delayed_job:start"
854
+ after "deploy:restart", "delayed_job:restart"
855
+ ```
856
+ Again, this configuration will be different for each instance, and so in our case each one is stored in different source repository branches.
857
+
858
+ Note that if you configure [Delayed::Job](https://github.com/collectiveidea/delayed_job) (or other backend) to work only from specific queues, you'll need to make sure that _any_ asynchronous tasks created by your application are submitted to one of those queues.
859
+
860
+ ## Dependencies
861
+
862
+ - Fragmentary is currently being used in production with Rails 6.0. Versions >=0.4 do not work with earlier versions of Rails for a couple of reasons:
863
+ - The version of ActiveJob in Rails 5.x does not support the custom serialization needed to enqueue asynchronous tasks for sending internal requests. A repository branch 'delayed_job' is available that uses the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem directly (i.e. without ActiveJob) and this works well with Rails 5.x. However, this branch is no longer maintained.
864
+ - The API for the Rails `ActionDispatch::Integration::Session` class, which is used to handle internal application requests, was changed in Rails 5.0. Consequently the current version of Fragmentary is incompatible with Rails 4.x and earlier. We do have a 'Rails.4.2' branch in the repository that uses the older API. However, this branch is no longer maintained.
865
+
866
+ ## Timestamps
772
867
 
773
- ## Testing
868
+ Fragmentary allows HTML comments to be optionally inserted into cached fragments identifying the time at which the fragment was last cached. Simply add `config.insert_timestamps = true` to the configuration block in your `config/initializers/fragmentary.rb` file.
774
869
 
775
- You're welcome to write some tests!!!
870
+ It is also possible to insert comments identifying a version name for the application code in use at the time the fragment was cached and the time at which that version was deployed. This is accomplished by dynamically adding values for the configuration settings `Fragmentary.config.release_name` and `Fragmentary.config.deployed_at` to your `config/initializers/fragmentary.rb` file at the time you deploy. ie. within your deployment script you need to arrange to append something like the following to that file.
871
+ ```
872
+ Fragmentary.config.release_name = release_name
873
+ Fragmentary.config.deployed_at = deployed_at
874
+ ```
875
+ It is up to you how you define/determine these values, but `deployed_at` would typically represent the value of `Time.now.utc` at the time the deployment script is executed. If these two configuration values are set, and `Fragmentary.config.insert_timestamps` is set to true, Fragmentary will automatically insert comments along the lines of the following:
876
+ ```
877
+ <!-- ProductDetails 2382 cached by Fragmentary version 0.4 at 2023-09-22 09:28:02 UTC -->
878
+ <!-- Cached using application release 20230919233149 deployed at 2023-09-19 23:33:16 UTC -->
879
+ ... Cached content appears here ...
880
+ <!-- ProductDetails 2382 ends -->
881
+ ```
882
+
883
+ ## Utility Methods
884
+
885
+ The `Fragment` class and its subclasses provide a number of utility instance methods that are typically useful for interacting with fragments from the console.
886
+ - `touch_or_destroy` - recursively touches the fragment record and all decendant fragments for which cache entries exist. Destroys any fragment records in the tree for which no cache entry exists.
887
+ - `cache_exist?` - returns a boolean indicating whether fragment has content stored in the cache.
888
+ - `content` - retrieves the fragment's cached content
889
+ - `delete_cache` - removes the fragment's content from the cache.
890
+ - `delete_cache_tree` - recursively removes from the cache all content for the fragment and its descendants.
891
+
892
+ ## Contributing
893
+
894
+ Bug reports and usage questions are welcome at https://github.com/MarkMT/fragmentary.
776
895
 
777
896
  ## License
778
897
 
data/fragmentary.gemspec CHANGED
@@ -22,9 +22,10 @@ Gem::Specification.new do |spec|
22
22
  spec.bindir = "exe"
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_runtime_dependency "rails", ">= 4.0.0", "< 5"
26
- spec.add_runtime_dependency "delayed_job_active_record", "~> 4.1"
25
+ spec.add_runtime_dependency "rails", "~> 6.0"
27
26
  spec.add_runtime_dependency "wisper-activerecord", "~> 1.0"
27
+ spec.add_runtime_dependency "http", "~> 3.0.0"
28
+ spec.add_runtime_dependency "nokogiri"
28
29
  spec.add_development_dependency "bundler", "~> 1.17"
29
30
  spec.add_development_dependency "rake", "~> 10.0"
30
31
  spec.add_development_dependency "rspec", "~> 3.0"
@@ -2,12 +2,18 @@ module Fragmentary
2
2
 
3
3
  class Config
4
4
  include Singleton
5
- attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path,
6
- :sign_out_path, :users, :default_user_type_mapping, :session_users
5
+ attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path, :sign_out_path,
6
+ :users, :default_user_type_mapping, :session_users, :application_root_url_column,
7
+ :remote_urls, :insert_timestamps, :deployed_at, :release_name
7
8
 
8
9
  def initialize
9
10
  # default
10
11
  @current_user_method = :current_user
12
+ @application_root_url_column = :application_root_url
13
+ @remote_urls = []
14
+ @insert_timestamps = false
15
+ @deployed_at = nil
16
+ @release_name = nil
11
17
  end
12
18
 
13
19
  def session_users=(session_users)
@@ -15,34 +21,47 @@ module Fragmentary
15
21
  Fragmentary.parse_session_users(session_users)
16
22
  @session_users = session_users
17
23
  end
24
+
25
+ def application_root_url_column=(column_name)
26
+ @application_root_url_column = column_name.to_sym
27
+ end
18
28
  end
19
29
 
20
30
  def self.current_user_method
21
31
  self.config.current_user_method
22
32
  end
23
33
 
24
- # Parse a class-specific set of session_user options
25
- # session_users can be an array of session_user keys, a hash of session_user definitions or an array
26
- # containing a mixture of both. The method should return an array of keys. If session_users is an
27
- # array, elements representing existing SessionUser objects should be included in the returned array.
34
+ # Parse a set of session_user options, creating session_users where needed, and return a set of user_type keys.
35
+ # session_users may take several forms:
36
+ # (1) a hash whose keys are user_type strings and whose values have the form {:credentials => credentials},
37
+ # where 'credentials' is either a hash of parameters to be submitted when logging in or a proc that
38
+ # returns those parameters.
39
+ # (2) an array of hashes as described in (1) above.
40
+ # (3) an array of user_type strings corresponding to SessionUser objects already defined.
41
+ # (4) an array containing a mixture of user_type strings and hashes as described in (1) above.
28
42
  # Non-hash elements that don't represent existing SessionUser objects should raise an exception. Array
29
- # elements that are hashes should be parsed to create new SessionUser objects. Raise an exception if
43
+ # elements that are hashes should be parsed to create new SessionUser objects. Raise an exception on
30
44
  # any attempt to redefine an existing user_type.
31
45
  def self.parse_session_users(session_users = nil)
32
46
  return nil unless session_users
33
47
  if session_users.is_a?(Array)
34
- # Fun fact: can't use 'each_with_object' here because 'acc += parse_session_users(v)' assigns a
35
- # different object to 'acc', while 'each_with_object' passes the *same* object to the block on
36
- # each iteration.
48
+ # Fun fact: can't use 'each_with_object' here because 'acc += parse_session_users(v)' would assign
49
+ # a different object to 'acc' on each iteration, while 'each_with_object' passes the *same* object
50
+ # to the block on each iteration.
37
51
  session_users.inject([]) do |acc, v|
38
52
  if v.is_a?(Hash)
39
53
  acc + parse_session_users(v)
40
54
  else
55
+ # v is a user_type, e.g. :admin
56
+ raise "No SessionUser exists for user_type '#{v}'" unless SessionUser.fetch(v)
41
57
  acc << v
42
58
  end
43
59
  end
44
60
  elsif session_users.is_a?(Hash)
45
61
  session_users.each_with_object([]) do |(k,v), acc|
62
+ # k is the user_type, v is an options hash that typically looks like {:credentials => login_credentials} where
63
+ # login_credentials is either a hash of parameters to be submitted at login or a proc that returns those parameters.
64
+ # In the latter case, the proc is executed when we actually log in to create a new session for the specified user.
46
65
  acc << k if user = SessionUser.new(k,v)
47
66
  end
48
67
  end