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 +5 -5
- data/CHANGELOG.md +13 -0
- data/README.md +180 -61
- data/fragmentary.gemspec +3 -2
- data/lib/fragmentary/config.rb +29 -10
- data/lib/fragmentary/fragment.rb +105 -37
- data/lib/fragmentary/fragments_helper.rb +19 -1
- data/lib/fragmentary/handler.rb +24 -0
- data/lib/fragmentary/{dispatcher.rb → jobs/dispatch_handlers_job.rb} +6 -6
- data/lib/fragmentary/jobs/send_requests_job.rb +26 -0
- data/lib/fragmentary/publisher.rb +2 -0
- data/lib/fragmentary/request.rb +1 -16
- data/lib/fragmentary/request_queue.rb +56 -32
- data/lib/fragmentary/serializers/handler_serializer.rb +24 -0
- data/lib/fragmentary/serializers/request_queue_serializer.rb +36 -0
- data/lib/fragmentary/session_user.rb +38 -0
- data/lib/fragmentary/subscription.rb +5 -2
- data/lib/fragmentary/user_session.rb +111 -33
- data/lib/fragmentary/version.rb +1 -1
- data/lib/fragmentary.rb +11 -0
- metadata +32 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 61a94b394035bb4b3ec317316e46fa77ca925cc15a162a76d6401500e6636b5d
|
4
|
+
data.tar.gz: 8db2f91e9a9b23f3ccdafbc2f800695e653426a656887959f807c80eca10c901
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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.
|
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,
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
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`
|
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.
|
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.
|
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
|
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
|
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
|
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
|
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
|
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,
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
788
|
+
### Support for Multiple Application Instances
|
762
789
|
|
763
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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", "
|
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"
|
data/lib/fragmentary/config.rb
CHANGED
@@ -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
|
-
:
|
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
|
25
|
-
# session_users
|
26
|
-
#
|
27
|
-
#
|
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
|
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)'
|
35
|
-
# different object to 'acc', while 'each_with_object' passes the *same* object
|
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
|