in_order 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +759 -0
- data/Rakefile +32 -0
- data/app/assets/config/in_order_manifest.js +2 -0
- data/app/assets/javascripts/in_order/application.js +15 -0
- data/app/assets/stylesheets/in_order/application.css +15 -0
- data/app/controllers/in_order/application_controller.rb +5 -0
- data/app/controllers/in_order/concerns/response_helpers.rb +33 -0
- data/app/controllers/in_order/elements_controller.rb +47 -0
- data/app/controllers/in_order/lists_controller.rb +86 -0
- data/app/helpers/in_order/application_helper.rb +4 -0
- data/app/jobs/in_order/application_job.rb +4 -0
- data/app/mailers/in_order/application_mailer.rb +6 -0
- data/app/models/in_order/add.rb +51 -0
- data/app/models/in_order/application_record.rb +5 -0
- data/app/models/in_order/aux/create_element.rb +17 -0
- data/app/models/in_order/aux/element_iterator.rb +39 -0
- data/app/models/in_order/aux/get_element.rb +17 -0
- data/app/models/in_order/aux/get_keys.rb +27 -0
- data/app/models/in_order/aux/keys.rb +58 -0
- data/app/models/in_order/aux/poly_find.rb +24 -0
- data/app/models/in_order/aux/poly_key.rb +117 -0
- data/app/models/in_order/aux/position_base.rb +25 -0
- data/app/models/in_order/aux/repair.rb +71 -0
- data/app/models/in_order/aux/sort_elements.rb +32 -0
- data/app/models/in_order/aux/var_keys.rb +20 -0
- data/app/models/in_order/create.rb +62 -0
- data/app/models/in_order/element.rb +119 -0
- data/app/models/in_order/fetch.rb +33 -0
- data/app/models/in_order/insert.rb +42 -0
- data/app/models/in_order/move.rb +15 -0
- data/app/models/in_order/purge.rb +71 -0
- data/app/models/in_order/queue.rb +65 -0
- data/app/models/in_order/remove.rb +29 -0
- data/app/models/in_order/stack.rb +12 -0
- data/app/models/in_order/trim.rb +74 -0
- data/app/models/in_order/update.rb +83 -0
- data/app/models/in_order.rb +9 -0
- data/app/views/in_order/lists/_list.html.erb +16 -0
- data/app/views/in_order/lists/index.html.erb +9 -0
- data/app/views/layouts/in_order/application.html.erb +16 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20190814101433_create_in_order_elements.rb +15 -0
- data/lib/in_order/engine.rb +5 -0
- data/lib/in_order/version.rb +3 -0
- data/lib/in_order.rb +5 -0
- data/lib/tasks/in_order_tasks.rake +4 -0
- metadata +119 -0
data/README.md
ADDED
@@ -0,0 +1,759 @@
|
|
1
|
+
|
2
|
+
In Order
|
3
|
+
========
|
4
|
+
|
5
|
+
Overview
|
6
|
+
--------
|
7
|
+
|
8
|
+
This is a (small simple single-purpose) Rails engine that,
|
9
|
+
in technical terms,
|
10
|
+
provides a uni-directional persistent linked list (presently SQL-based).
|
11
|
+
|
12
|
+
It's to establish one-to-many relationships between a composite *key*
|
13
|
+
and an ordered collection of *ActiveRecord* models.
|
14
|
+
|
15
|
+
The *key* is a combination of a single *ActiveRecord* model and a
|
16
|
+
free-text string, one of which is mandatory.
|
17
|
+
|
18
|
+
It allows you to introduce ad hoc relationships between arbitrary records
|
19
|
+
without changing anything in the participating records themselves.
|
20
|
+
|
21
|
+
Uses
|
22
|
+
----
|
23
|
+
|
24
|
+
Its potential uses are manifold, in fact, there are too many to mention.
|
25
|
+
In some guise or other, the basic functionality is used everywhere,
|
26
|
+
and it's a foundation of many patterns.
|
27
|
+
|
28
|
+
In everyday web development, the API will mainly be used to
|
29
|
+
store particular users' preferences, selections and actions.
|
30
|
+
Especially if preserving the order of the related elements is important.
|
31
|
+
|
32
|
+
Some Use-Cases
|
33
|
+
--------------
|
34
|
+
|
35
|
+
The API could be used to support the development of the following:
|
36
|
+
|
37
|
+
- Listing a user's favoured or recently accessed records.
|
38
|
+
So that they may, for example,
|
39
|
+
appear at the top of various lists used in HTML elements,
|
40
|
+
such as *select options* or *table* rows or menu links.
|
41
|
+
|
42
|
+
- For session storage of things like search terms, bookmarked links,
|
43
|
+
current records and notifications.
|
44
|
+
|
45
|
+
- Displaying user-specific recommendations, linking users to groups,
|
46
|
+
intermediate storage for multi-page wizards, tagging, contextual lists,
|
47
|
+
queuing jobs, auditing, tracking changes, etc..
|
48
|
+
|
49
|
+
- To log things like page clicks, form submissions, login attempts,
|
50
|
+
API calls, record edits & IP addresses by user, etc..
|
51
|
+
|
52
|
+
- Recording messages or conversations.
|
53
|
+
|
54
|
+
- Drag'n'drop lists, for which a rudementary *Stimumlus* module is included.
|
55
|
+
This is for things like to-do lists,
|
56
|
+
task or page ordering,
|
57
|
+
allowing users to specify menu items,
|
58
|
+
and so on.
|
59
|
+
|
60
|
+
Actually, the API can be used for all sorts of aggregation, notably,
|
61
|
+
if the underlying associations need to be kept in a particular sequence
|
62
|
+
and/or are volatile or temporary or experimental.
|
63
|
+
|
64
|
+
Benefits and Shortcomings
|
65
|
+
-------------------------
|
66
|
+
|
67
|
+
Although this facility carries an inherent inefficiency -
|
68
|
+
it creates a new *join table* row for each linkage -
|
69
|
+
it has the advantage of unobtrusiveness, that is,
|
70
|
+
of not affecting, in any way, the records it links.
|
71
|
+
They remain fully intact.
|
72
|
+
|
73
|
+
Note that there are plenty of other ways
|
74
|
+
to keep a discrete group of models in a specific order,
|
75
|
+
for example *ActiveRecord scopes* and third-party
|
76
|
+
facilities like *acts\_as\_list*.
|
77
|
+
In the majority of cases,
|
78
|
+
one of these will be more suitable than this facility,
|
79
|
+
which requires that each sequence be explicitly set up,
|
80
|
+
element by element.
|
81
|
+
|
82
|
+
An installation of this engine requires only one major change to your
|
83
|
+
existing environment, namely, the addition of an SQL migration
|
84
|
+
to create a table, *in\_order\_elements*.
|
85
|
+
The API is accessible from within its own engine,
|
86
|
+
which has no external dependencies apart from Rails.
|
87
|
+
This means that you can give it a try with
|
88
|
+
minimal setup and an easy rollback.
|
89
|
+
|
90
|
+
By far, the thorniest issue in using this facility
|
91
|
+
is getting rid of redundant associations.
|
92
|
+
A few ways to avert and rectify this are explained below.
|
93
|
+
|
94
|
+
Contents
|
95
|
+
--------
|
96
|
+
|
97
|
+
This engine consists of the following:
|
98
|
+
|
99
|
+
1. A single SQL table, *in\_order\_elements*.
|
100
|
+
2. An extensive API, which is composed of around two dozen Ruby classes.
|
101
|
+
3. Two REST controllers providing limited API access.
|
102
|
+
4. A prototype *Stimulus* module for a drag'n'drop list.
|
103
|
+
|
104
|
+
The last two items are quite specialised,
|
105
|
+
so are less likely to satisfy any specific requirements you have.
|
106
|
+
|
107
|
+
It was developed with *Ruby 2.6.0* and *Rails 5.2.3*.
|
108
|
+
|
109
|
+
API Introduction
|
110
|
+
----------------
|
111
|
+
|
112
|
+
The public API comprises of a dozen Ruby classes that provide
|
113
|
+
a full gamut of ways to manipulate the elements of a list.
|
114
|
+
|
115
|
+
Most of these classes perform a simple atomic operation,
|
116
|
+
and have only a few input parameters and options.
|
117
|
+
|
118
|
+
Sometimes, you may only need to call two of the supplied classes -
|
119
|
+
one to update and one to retrieve - *both short one-line calls*.
|
120
|
+
|
121
|
+
In more complicated cases,
|
122
|
+
you may need to make a few calls to these classes in succession.
|
123
|
+
|
124
|
+
> The *minitest* unit tests cover the API usage comprehensively,
|
125
|
+
> but these tests go into more detail than you'll need!
|
126
|
+
|
127
|
+
API Calls
|
128
|
+
---------
|
129
|
+
|
130
|
+
Although the API has lots of classes,
|
131
|
+
for the most part, they work in similar ways,
|
132
|
+
i.e. in the parameters they accept,
|
133
|
+
and in the tasks they perform (there is a fair degree of overlap).
|
134
|
+
|
135
|
+
Nearly all of the calls can be made in isolation
|
136
|
+
with a single (Ruby) statement,
|
137
|
+
and for most requirements,
|
138
|
+
you'll only need to use a small subset of the available classes.
|
139
|
+
|
140
|
+
They fall into three main categories:
|
141
|
+
|
142
|
+
1. To retrieve the elements of an existing list, by supplying a composite *key*.
|
143
|
+
|
144
|
+
2. To create or update a list as a whole, by supplying a composite *key*.
|
145
|
+
|
146
|
+
3. To change a list by dealing with individual elements.
|
147
|
+
|
148
|
+
Identifying Lists
|
149
|
+
-----------------
|
150
|
+
|
151
|
+
As said, many of the API calls require a special unique *key*,
|
152
|
+
which is used to access individual lists.
|
153
|
+
|
154
|
+
This *key* has two components:
|
155
|
+
|
156
|
+
1. An instance of an *ActiveRecord* model,
|
157
|
+
or else a reference to one by some sort of pairing of
|
158
|
+
a *type* name and an (SQL) *id*.
|
159
|
+
|
160
|
+
2. A string, which, in practice, will be a general classifier,
|
161
|
+
such as the name of a section, group, branch, label, tag, etc..
|
162
|
+
|
163
|
+
You must give either one or both.
|
164
|
+
|
165
|
+
> Internally, the composite *key* is represented by the class,
|
166
|
+
> *InOrder::Aux::Keys* (aka *InOrder::Key*),
|
167
|
+
> but you shouldn't have to refer to this directly.
|
168
|
+
> Instead, you specify the actual *key* values as one or two parameters,
|
169
|
+
> which are passed to the constructor of the aforesaid class internally.
|
170
|
+
|
171
|
+
The Element Class
|
172
|
+
-----------------
|
173
|
+
|
174
|
+
This is an *ActiveRecord* model, which links the *key*
|
175
|
+
to an associated record.
|
176
|
+
|
177
|
+
#### Fields
|
178
|
+
|
179
|
+
Its name is *InOrder::Element*, and it has the fields:
|
180
|
+
|
181
|
+
1. *owner* which is the initial key component,
|
182
|
+
it's a polymorphically referenced model.
|
183
|
+
|
184
|
+
2. *scope* which is the final key component,
|
185
|
+
it's a string designating some kind of category.
|
186
|
+
|
187
|
+
3. *subject* which is a related record (usually one of many),
|
188
|
+
it's also a polymorphically referenced model.
|
189
|
+
|
190
|
+
4. *element\_id* which points to the next Element of the list.
|
191
|
+
|
192
|
+
#### Scopes
|
193
|
+
|
194
|
+
For these four fields, there are corresponding
|
195
|
+
*(ActiveRecord) scope* definitions,
|
196
|
+
which are prefixed with **by_**.
|
197
|
+
So, for example, you can find elements that have a certain linked record with:
|
198
|
+
|
199
|
+
- `InOrder::Element.by_subject(subject)`
|
200
|
+
|
201
|
+
#### Class methods
|
202
|
+
|
203
|
+
Also, there are a number of class methods that
|
204
|
+
that return details about a list,
|
205
|
+
and others, with wider usefulness,
|
206
|
+
that delete an entire list in one go.
|
207
|
+
|
208
|
+
_What you're allowed to give as the **key** is explained below._
|
209
|
+
|
210
|
+
- To see a list's length, use something like:
|
211
|
+
|
212
|
+
- `InOrder::Element.find_by_key(key).count`
|
213
|
+
|
214
|
+
- To retrieve a list's first or last item:
|
215
|
+
|
216
|
+
- `InOrder::Element.first_element(key)`
|
217
|
+
- `InOrder::Element.last_element(key)`
|
218
|
+
|
219
|
+
- To check if a list includes a particular model:
|
220
|
+
|
221
|
+
- `InOrder::Element.has_subject?(key) { a_model_to_check }`
|
222
|
+
|
223
|
+
You specify the model to look for in a block,
|
224
|
+
this is because the *key* can be given as varied arguments.
|
225
|
+
You can also specify a reference to a model with a *type & id*
|
226
|
+
partnership, these formats are shown below.
|
227
|
+
|
228
|
+
- Finally, to delete a complete list, you have a few choices:
|
229
|
+
|
230
|
+
- `InOrder::Element.delete_elements(key)`
|
231
|
+
- `InOrder::Element.delete_list(key)`
|
232
|
+
- `InOrder::Element.by_keys(key).delete_all`
|
233
|
+
|
234
|
+
The first deletes the elements one at a time,
|
235
|
+
the last two are equivalent.
|
236
|
+
|
237
|
+
#### Iterator
|
238
|
+
|
239
|
+
Additionally, there is another class, `InOrder::Iterator.new(key)`,
|
240
|
+
that behaves like a regular Ruby *Enumerable*,
|
241
|
+
that is, the method *each* yields an *Element* in turn.
|
242
|
+
You could use this for random access with, say, an integer index (offset).
|
243
|
+
_Be restrained in using this, though, because it hits the database hard._
|
244
|
+
|
245
|
+
Retrieve List Items
|
246
|
+
-------------------
|
247
|
+
|
248
|
+
The principal API class to access a list is *InOrder::Fetch*,
|
249
|
+
which returns an Array of models, obviously, in the stated order.
|
250
|
+
|
251
|
+
_This does eager fetching of the linked models,
|
252
|
+
so reducing the request to a couple of SQL queries._
|
253
|
+
|
254
|
+
This API call takes just a *key* as input,
|
255
|
+
the following examples show the differing ways of specifying this.
|
256
|
+
|
257
|
+
**These different formats also apply to the other
|
258
|
+
API calls requiring a *key* as an input parameter.**
|
259
|
+
|
260
|
+
These invocations are all valid,
|
261
|
+
and, except for the last four, would all produce the same results:
|
262
|
+
|
263
|
+
- `InOrder::Fetch.new(User.find(999), 'friends').call`
|
264
|
+
- `InOrder::Fetch.new([ 'User', 999 ], 'friends').call`
|
265
|
+
- `InOrder::Fetch.new('User-999', 'friends').call`
|
266
|
+
- `InOrder::Fetch.new({ type: 'User', id: '999' }, 'friends').call`
|
267
|
+
- `InOrder::Fetch.new(owner: User.find(999), scope: 'friends').call`
|
268
|
+
- `InOrder::Fetch.new(InOrder::Key.new(User.find(999), 'friends')).elements`
|
269
|
+
- `InOrder::Fetch.new('User 999').call`
|
270
|
+
- `InOrder::Fetch.new(owner: 'User:999').call`
|
271
|
+
- `InOrder::Fetch.new(owner: { type: 'User', id: '999' }).call`
|
272
|
+
- `InOrder::Fetch.new(scope: 'friends').call`
|
273
|
+
|
274
|
+
The method *call* returns the linked records themselves.
|
275
|
+
|
276
|
+
If you want the linking Element records instead,
|
277
|
+
you use the method, *elements*, in place of *call*.
|
278
|
+
|
279
|
+
As said, the Element class has an instance method, *subject*,
|
280
|
+
which returns the linked record.
|
281
|
+
|
282
|
+
Create a List
|
283
|
+
-------------
|
284
|
+
|
285
|
+
When adding records to a list, you can give actual instances,
|
286
|
+
or give references like:
|
287
|
+
|
288
|
+
- `[ 'Type', 999 ]`
|
289
|
+
- `{ type: 'Type', id: 999 }`
|
290
|
+
- `"Type-999"`
|
291
|
+
|
292
|
+
### Adding multiple records at once
|
293
|
+
|
294
|
+
There are two classes for creating a list
|
295
|
+
with more than one associated record.
|
296
|
+
These are *InOrder::Create* and *InOrder::Update*,
|
297
|
+
the latter performs other tasks as well, and is explained later on.
|
298
|
+
|
299
|
+
- `InOrder::Create.new(User.find(999), 'friends').call(a_prepared_list_of_friends)`
|
300
|
+
|
301
|
+
If you make either of these API calls (*Create* or *Update*)
|
302
|
+
when a list (with an identical key) pre-exists,
|
303
|
+
then the new records will, by default, be appended to the original items,
|
304
|
+
that is, docked onto the end.
|
305
|
+
If the option, *append*, is set to *false*,
|
306
|
+
then the new items will be prepended instead, as in:
|
307
|
+
|
308
|
+
- `InOrder::Create.new(User.find(999), 'friends').call(a_prepared_list_of_friends, append: false)`
|
309
|
+
|
310
|
+
These methods return the linking Element records,
|
311
|
+
not the records that are actually linked.
|
312
|
+
|
313
|
+
And, importantly, for *Create* (but not *Update*) the returned list will not
|
314
|
+
include any of the items from a previous list - only the ones you just added.
|
315
|
+
|
316
|
+
> Be aware that you can only add *ActiveRecord* models as list item subjects.
|
317
|
+
|
318
|
+
### Adding a single record to a list
|
319
|
+
|
320
|
+
To add one new record to either the top or bottom of a list:
|
321
|
+
|
322
|
+
- `InOrder::Add.new(User.find(999), 'friends').call(a_friend)`
|
323
|
+
|
324
|
+
This will append *a\_friend* to an existing list.
|
325
|
+
If there's no list present already,
|
326
|
+
it'll become the first (and last) element.
|
327
|
+
|
328
|
+
To prepend the record, i.e. put it at the beginning:
|
329
|
+
|
330
|
+
- `InOrder::Add.new(User.find(999), 'friends').call(a_friend, at: top)`
|
331
|
+
|
332
|
+
You can also use the methods: *prepend* and *append*, in place of *call*.
|
333
|
+
The *at* option is omitted when invoking *prepend*, e.g.
|
334
|
+
|
335
|
+
- `InOrder::Add.new(User.find(999), 'friends').prepend(a_friend)`
|
336
|
+
|
337
|
+
These *Add* calls return the new *Element*, which wraps the added record,
|
338
|
+
but, by itself, this won't normally be useful.
|
339
|
+
|
340
|
+
Although you can add just one item in the *Create* call, mentioned above,
|
341
|
+
using *Add* is a bit more efficient,
|
342
|
+
and it has the following extra feature.
|
343
|
+
|
344
|
+
#### Adding a record in the middle
|
345
|
+
|
346
|
+
Also, but with less efficiency and more difficulty,
|
347
|
+
you can put a new item at any position by adding a block of custom code,
|
348
|
+
in which you step through the list to find a particular place to go.
|
349
|
+
Here's a rough contrived example:
|
350
|
+
|
351
|
+
```
|
352
|
+
InOrder::Add.new(a_key).insert(a_record) do |iterator, a_record|
|
353
|
+
# This returns the element that a_record will be put after
|
354
|
+
iterator.find(iterator.first) do |element|
|
355
|
+
element.subject == a_record
|
356
|
+
end
|
357
|
+
end
|
358
|
+
```
|
359
|
+
_As you can see, this is convoluted, so in nearly all cases
|
360
|
+
it'll be simpler to position a new item using *Insert*, shown below._
|
361
|
+
|
362
|
+
### *Singing and dancing* record addition
|
363
|
+
|
364
|
+
If you want, for instance,
|
365
|
+
to add one or more models to an existing list at the beginning,
|
366
|
+
to ensure that the list has a length no bigger than *6*,
|
367
|
+
and that no items re-occur, use:
|
368
|
+
|
369
|
+
- `InOrder::Update.new(User.find(999), 'friends').call(an_array_of_some_fiends, append: false, max: 6, uniq: true)`
|
370
|
+
|
371
|
+
This *Update* call will return the whole new list, as elements.
|
372
|
+
If you want the linked records instead, use *subjects* in place of *call*.
|
373
|
+
|
374
|
+
If *uniq* is given as *false* then any re-occurrences (in the original list)
|
375
|
+
of the added models will remain in place, (i.e. not be removed from the list).
|
376
|
+
The default setting is *true*, so be sure to specify (*uniq: false*) if
|
377
|
+
you want to keep repetitions.
|
378
|
+
|
379
|
+
Remove List Items
|
380
|
+
-----------------
|
381
|
+
|
382
|
+
As you'd expect, all of the following deletion operations only remove
|
383
|
+
the linking (wrapper) records, **never** the (underlying) records
|
384
|
+
that are linked together.
|
385
|
+
|
386
|
+
Likewise, if you delete a model that had been previously linked,
|
387
|
+
the (surviving) list *Element* will be left with a *dangling reference*,
|
388
|
+
i.e. it'll be an orphan, and be worse than useless,
|
389
|
+
as it may give rise to malfunctions or even fatal errors.
|
390
|
+
|
391
|
+
There are a number of straight-forward ways of preventing this,
|
392
|
+
(e.g. with *dependent: :destroy*, or inside of an *after\_destroy* callback),
|
393
|
+
but they do involve changing code of the model that's being linked.
|
394
|
+
|
395
|
+
In emergencies, you can search for and destroy these erroneous records, with:
|
396
|
+
|
397
|
+
- `InOrder::Repair.new.call`
|
398
|
+
|
399
|
+
If you wish to root them out without deleting, try:
|
400
|
+
|
401
|
+
- `InOrder::Repair.ic`
|
402
|
+
|
403
|
+
For ultra-caution, you can bolster your fetch requests
|
404
|
+
by sticking an integrity fix into the mix.
|
405
|
+
It'd go like something like this:
|
406
|
+
|
407
|
+
- `InOrder::Fetch.new(User.find(999)).repair.call`
|
408
|
+
|
409
|
+
But this (*repair*) invocation is slow and only
|
410
|
+
really suggested as a stop-gap measure.
|
411
|
+
|
412
|
+
> A *subscribe and publish* mechanism, (activated on a model's destruction)
|
413
|
+
> would resolve this issue. *This may be added in future.*
|
414
|
+
|
415
|
+
### Deleting elements
|
416
|
+
|
417
|
+
Take note that removing an Element is not so simple as just deleting a
|
418
|
+
single row from the (elements) table.
|
419
|
+
Since there'll be a reference to the deleted Element
|
420
|
+
if a previous Element exists, and conversely,
|
421
|
+
the deleted Element might have a reference to a further element.
|
422
|
+
|
423
|
+
#### Delete by subject(s)
|
424
|
+
|
425
|
+
To drop elements from a *keyed* list that have an existing link
|
426
|
+
to one or more given models, (in this example, *enemies*):
|
427
|
+
|
428
|
+
- `InOrder::Purge.new(User.find(999), 'friends').remove(some_enemies)`
|
429
|
+
|
430
|
+
#### Mass destruction of elements
|
431
|
+
|
432
|
+
For the reason given directly above - on orphan eradication
|
433
|
+
*(Please don't presume I advocate this as social policy!)* -
|
434
|
+
when you delete the subject of a link,
|
435
|
+
you may want to make the following call alongside:
|
436
|
+
|
437
|
+
- `InOrder::Purge.delete_by_subject(a_deleted_model)`
|
438
|
+
|
439
|
+
This will unlink (and remove) all Elements that wrap *a\_deleted\_model*,
|
440
|
+
regardless of the lists the (deleted model's) Elements belong to.
|
441
|
+
|
442
|
+
### Discarding duplicates
|
443
|
+
|
444
|
+
To ensure that there are no two (linked) models the same in a given list:
|
445
|
+
|
446
|
+
- `InOrder::Purge.new(User.find(999), 'friends').uniq(keep_last: true)`
|
447
|
+
|
448
|
+
If the *keep_last* option is left out, or is *false*,
|
449
|
+
it'll retain the first occurrence of a repeated model in a given list.
|
450
|
+
The method *call* is an alias for *uniq*.
|
451
|
+
|
452
|
+
### Cutting a list down to size
|
453
|
+
|
454
|
+
Ensures that a list does not exceed a given maximum size (ceiling).
|
455
|
+
|
456
|
+
In this example, the maximum's *12*, the unwanted items are removed towards
|
457
|
+
the start, and not permanently dropped from the list:
|
458
|
+
|
459
|
+
- `InOrder::Trim.new(12, destroy: false, take_from: top).call(a_list_of_elements)`
|
460
|
+
|
461
|
+
Note that this call does not take an identifying *key* as input,
|
462
|
+
rather, it takes a list of *Elements*,
|
463
|
+
obtained by calling *InOrder::Fetch.new(a_key).elements*.
|
464
|
+
|
465
|
+
And also that the default is to remove the excess elements for good,
|
466
|
+
so you must add **destroy: false** to avoid this.
|
467
|
+
|
468
|
+
This call returns a new (duplicated and abridged) list of Elements.
|
469
|
+
|
470
|
+
#### Cutting down on the number of calls
|
471
|
+
|
472
|
+
To fetch and trim a list (temporarily), returning the linked records,
|
473
|
+
you may be tempted to try something like this:
|
474
|
+
|
475
|
+
```
|
476
|
+
MAX = 12
|
477
|
+
|
478
|
+
key = InOrder::Key.new(current_user, 'friends')
|
479
|
+
|
480
|
+
elements = InOrder::Fetch.new(key).elements
|
481
|
+
|
482
|
+
InOrder::Trim.new(MAX).call(elements, destroy: false).map(&:subject)
|
483
|
+
```
|
484
|
+
|
485
|
+
But you can achieve the same, with less verbosity *(sidestepping RSI)*,
|
486
|
+
using the shortcuts:
|
487
|
+
|
488
|
+
- `InOrder::Trim.set_max(12).call(current_user, 'friends')`
|
489
|
+
|
490
|
+
Whereupon the maximum will be temporarily stored,
|
491
|
+
obviating the need to call *set_max* again,
|
492
|
+
if you, soon afterwards, make a similar call, as in:
|
493
|
+
|
494
|
+
- `InOrder::Trim.(current_user, 'best-friends')`
|
495
|
+
|
496
|
+
Alternatively, to trim from the beginning
|
497
|
+
and with permanent element removal, you may use:
|
498
|
+
|
499
|
+
- `InOrder::Trim.call(current_user, 'friends') { [ 12, take_from: :top, destroy: true ] }`
|
500
|
+
|
501
|
+
Here, the default for *destroy* is flipped
|
502
|
+
(in the longer form it's *true*),
|
503
|
+
and the block returns the argument list given to Trim's constructor.
|
504
|
+
|
505
|
+
> These (Trim) calls raise an exception if you attempt to 'destroy',
|
506
|
+
> and you also give a list of items that aren't *Element* types.
|
507
|
+
|
508
|
+
Stacks and Queues
|
509
|
+
-----------------
|
510
|
+
|
511
|
+
These are typically data structures with a short lifespan.
|
512
|
+
|
513
|
+
In both of them, there is also a *peek* method that shows the next
|
514
|
+
item without making a removal.
|
515
|
+
|
516
|
+
### First in, first out
|
517
|
+
|
518
|
+
To add an item:
|
519
|
+
|
520
|
+
- `InOrder::Stack.new(User.find(999), 'friends').push(a_model)`
|
521
|
+
|
522
|
+
To retrieve an item:
|
523
|
+
|
524
|
+
- `InOrder::Stack.new(User.find(999), 'friends').pop
|
525
|
+
|
526
|
+
### First in, last out
|
527
|
+
|
528
|
+
To add an item:
|
529
|
+
|
530
|
+
- `InOrder::Queue.new(User.find(999), 'friends').join(Club.find(999))`
|
531
|
+
|
532
|
+
The method *join* has an alias of *add*.
|
533
|
+
|
534
|
+
To retrieve an item:
|
535
|
+
|
536
|
+
- `InOrder::Queue.new(User.find(999), 'friends').call
|
537
|
+
|
538
|
+
The method *call* has an alias of *leave* and *pull*.
|
539
|
+
|
540
|
+
### Fixed size
|
541
|
+
|
542
|
+
When you put a new item onto these lists,
|
543
|
+
you may specify, as a second argument,
|
544
|
+
a length that the list will not go beyond.
|
545
|
+
For example, `InOrder::Stack.new(key).push(model, 7)`.
|
546
|
+
|
547
|
+
> The behaviour of these two classes can be replicated
|
548
|
+
> using other API calls. They are for convenience.
|
549
|
+
|
550
|
+
Operations on List Elements
|
551
|
+
---------------------------
|
552
|
+
|
553
|
+
The following calls do not take a *key* as input to the constructor.
|
554
|
+
Here, you supply references to the individual Elements that make up a list.
|
555
|
+
|
556
|
+
Unlike the preceding, these calls give you full control over
|
557
|
+
the positioning of Elements in an existing list.
|
558
|
+
|
559
|
+
There are three parameters used to add or move a certain item:
|
560
|
+
|
561
|
+
1. *target* which is an actual Element that'll be moved or added.
|
562
|
+
|
563
|
+
2. *marker* is an Element that indicates the new position.
|
564
|
+
|
565
|
+
3. *adjacency* has a value of 'after' (default) or 'before',
|
566
|
+
and says whether the *target* will go ahead of,
|
567
|
+
or behind, the *marker* Element.
|
568
|
+
|
569
|
+
Both *target* and *marker* need to be instances of *InOrder::Element*,
|
570
|
+
or else be (SQL) *id's* of such.
|
571
|
+
|
572
|
+
### Moving an existing item
|
573
|
+
|
574
|
+
- `InOrder::Move.new(target, marker, adjacency).call`
|
575
|
+
|
576
|
+
### Positioning a new item
|
577
|
+
|
578
|
+
- `InOrder::Insert.new(target, marker, adjacency).call`
|
579
|
+
|
580
|
+
If you don't have the new record already wrapped in an Element,
|
581
|
+
which will be the most common case, use:
|
582
|
+
|
583
|
+
- `InOrder::Insert.call(a_model_to_be_linked, marker, adjacency)`
|
584
|
+
|
585
|
+
### Deleting an item
|
586
|
+
|
587
|
+
- `InOrder::Remove.new(target).call`
|
588
|
+
|
589
|
+
Controllers
|
590
|
+
-----------
|
591
|
+
|
592
|
+
There are two REST controllers included as well.
|
593
|
+
You can can use them for JSON output,
|
594
|
+
or to alter a list from an *Ajax* request.
|
595
|
+
|
596
|
+
However, at present the views they have,
|
597
|
+
in the engine's *dummy* Rails app,
|
598
|
+
are inaccessible from your own app, and besides they're crude,
|
599
|
+
but they may potentially be used as rough starter templates.
|
600
|
+
|
601
|
+
- *InOrder::ListsController* this accepts the said composite *key*
|
602
|
+
as parameters, and will fetch a list and add & delete items.
|
603
|
+
|
604
|
+
- *InOrder::ElementsController* this accepts Element id's as parameters
|
605
|
+
and allows more control over the placement of Elements.
|
606
|
+
The *drag'n'drop* list, outlined just below, uses this controller.
|
607
|
+
|
608
|
+
Generally, these controllers won't be need to be used as much as the,
|
609
|
+
already described, Ruby API, which is more direct, concise and flexible.
|
610
|
+
|
611
|
+
A Drag'n'Drop List
|
612
|
+
------------------
|
613
|
+
|
614
|
+
The engine has, hidden away,
|
615
|
+
a little *Stimulus* module that implements a *drag'n'drop* list.
|
616
|
+
|
617
|
+
This consists of three new files:
|
618
|
+
|
619
|
+
1. *test/dummy/app/javascript/controllers/drag\_drop\_controller.js*
|
620
|
+
2. *test/dummy/app/javascript/controllers/list\_controller.js*
|
621
|
+
3. *app/views/in\_order/lists/\_list.html.erb*
|
622
|
+
|
623
|
+
This code is minimal and will need changing.
|
624
|
+
The third probably a lot.
|
625
|
+
The second a bit as it shows debugging info.
|
626
|
+
The first probably won't need altering at all.
|
627
|
+
|
628
|
+
This list works in the engine's *dummy* testing Rails app,
|
629
|
+
which, for now, is where you'll have to go to extract these files.
|
630
|
+
If you decide to do this, get the source from:
|
631
|
+
[github.com/srycyk/in_order](https://github.com/srycyk/in_order)
|
632
|
+
|
633
|
+
> It's hard to package this up at present,
|
634
|
+
> since Rails is in flux over the way that JS assets are prepared,
|
635
|
+
> that is, whether they be handled by Sprockets or Webpacker.
|
636
|
+
> Also, it needs *Stimulus* installed - but not *jQuery*.
|
637
|
+
|
638
|
+
General Remarks
|
639
|
+
---------------
|
640
|
+
|
641
|
+
#### What's a linked list?
|
642
|
+
|
643
|
+
A broad definition of a *linked list* may go thus:
|
644
|
+
it's is a chain of elements,
|
645
|
+
each of which contain both a reference to an opaque data item,
|
646
|
+
and also a pointer to the next element in the sequence.
|
647
|
+
The final element typically has its pointer valued as null.
|
648
|
+
|
649
|
+
However, in this particular implementation,
|
650
|
+
the element also contains a *key*.
|
651
|
+
|
652
|
+
#### Polymorphic reliance
|
653
|
+
|
654
|
+
Purists may demur at the unabashed use of *polymorphic* types.
|
655
|
+
|
656
|
+
But since these (associative) records are, in this case,
|
657
|
+
the leaves of a tree, (never part of a branch),
|
658
|
+
there is next to no chance of untoward issues surfacing.
|
659
|
+
And the alternative of crafting every particular relationship with
|
660
|
+
literal *table* names is too long-winded,
|
661
|
+
and much more bother to develop and maintain.
|
662
|
+
|
663
|
+
#### Demonstration
|
664
|
+
|
665
|
+
Inside of the engine, in the sub-directory *test/dummy/*,
|
666
|
+
there's a Raila app.
|
667
|
+
Apart from supporting the tests, this app has some scaffolding code
|
668
|
+
added that allows you to link records together manually in web pages.
|
669
|
+
To have a go, `cd test/dummy/`, run migrations & seed, `rails s`, etc..
|
670
|
+
|
671
|
+
#### Multiple databases
|
672
|
+
|
673
|
+
The table behind this model, *in\_order\_elements*,
|
674
|
+
could be put on a separate database,
|
675
|
+
but it'd make the eager fetching tricky.
|
676
|
+
|
677
|
+
But if you're going to use this facility just to gather statistics,
|
678
|
+
doing this would certainly boost the overall performance.
|
679
|
+
|
680
|
+
Getting Started
|
681
|
+
---------------
|
682
|
+
|
683
|
+
Add a *Gemfile* entry, with one of these:
|
684
|
+
|
685
|
+
```ruby
|
686
|
+
gem 'in_order'
|
687
|
+
|
688
|
+
gem 'in_order', git: 'https://github.com/srycyk/in_order'
|
689
|
+
```
|
690
|
+
|
691
|
+
And then execute:
|
692
|
+
|
693
|
+
```bash
|
694
|
+
$ bundle
|
695
|
+
```
|
696
|
+
|
697
|
+
Mount the engine in *config/routes.rb* with:
|
698
|
+
|
699
|
+
```ruby
|
700
|
+
mount InOrder::Engine, at: "/in_order"
|
701
|
+
```
|
702
|
+
|
703
|
+
Copy the migration, from the engine, over into your own app:
|
704
|
+
|
705
|
+
```bash
|
706
|
+
$ rake in_order:install:migrations
|
707
|
+
```
|
708
|
+
|
709
|
+
And create the table, *in\_order\_elements*:
|
710
|
+
|
711
|
+
```bash
|
712
|
+
$ rake db:migrate
|
713
|
+
```
|
714
|
+
|
715
|
+
Getting Finished
|
716
|
+
----------------
|
717
|
+
|
718
|
+
Rollback migration (just for the engine):
|
719
|
+
|
720
|
+
```bash
|
721
|
+
$ rails db:migrate SCOPE=in_order VERSION=0
|
722
|
+
```
|
723
|
+
|
724
|
+
Delete the engine's migration file:
|
725
|
+
|
726
|
+
```bash
|
727
|
+
$ git rm db/migrate/*elements.in_order.rb
|
728
|
+
```
|
729
|
+
|
730
|
+
Remove the *Gemfile* entry.
|
731
|
+
|
732
|
+
Remove the *config/routes.rb* entry.
|
733
|
+
|
734
|
+
Then, to get back to where you started:
|
735
|
+
|
736
|
+
```bash
|
737
|
+
$ bundle
|
738
|
+
```
|
739
|
+
|
740
|
+
Licence
|
741
|
+
-------
|
742
|
+
|
743
|
+
The gem is available as open source under the terms of
|
744
|
+
the [MIT License](https://opensource.org/licenses/MIT).
|
745
|
+
|
746
|
+
|
747
|
+
Contributing
|
748
|
+
------------
|
749
|
+
|
750
|
+
Since this facility provides just one basic function,
|
751
|
+
(frankly, *it's a one-trick pony, flogged for all it's worth*),
|
752
|
+
it's unlikely that it will need extending or forking.
|
753
|
+
It's more a utility component to be used a building-block.
|
754
|
+
|
755
|
+
|
756
|
+
If you spot any bugs,
|
757
|
+
or if you have any issues, queries or suggestions,
|
758
|
+
please mail me at stephen.rycyk@googlemail.com
|
759
|
+
|