support_table 1.0.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/CHANGELOG.md +11 -0
- data/MIT-LICENSE +20 -0
- data/README.md +377 -0
- data/VERSION +1 -0
- data/lib/support_table/belongs_to_support_table.rb +29 -0
- data/lib/support_table/definition.rb +81 -0
- data/lib/support_table.rb +117 -0
- data/support_table.gemspec +42 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e6446caaf1ed0561e4fc98214a72e30fae6fc70aa7ef868aa6e6a17f4dc4f621
|
|
4
|
+
data.tar.gz: 9f7739af82443b3279bf74cb1262370284fc0c3554f42a3dc1d5d254c176d4a5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ddb606d4d8674bb335dad19888ee531aafe2d8790396bbfde7ab6ea574fb20b3b01aa0d599ac2f43720d6ecca1709f0318c2ab42f4f9749c259f1b75462ae451
|
|
7
|
+
data.tar.gz: 22cfa034d8062e57d5e7eff49a532f97d293bb2a7847392f57388ebc7e40530df91dfd452be58131e45c3fd6798a39bcb3215a5d8775307e878e0fd19327ac05
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## 1.0.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Initial release.
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 Brian Durand
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Support Table
|
|
2
|
+
|
|
3
|
+
[](https://github.com/bdurand/support_table/actions/workflows/continuous_integration.yml)
|
|
4
|
+
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](https://badge.fury.io/rb/support_table)
|
|
6
|
+
|
|
7
|
+
This gem builds on top of the [support_table_data](https://github.com/bdurand/support_table_data) and [support_table_cache](https://github.com/bdurand/support_table_cache) gems to provide a pre-configured, drop-in solution for maintaining and using support tables in a Rails application.
|
|
8
|
+
|
|
9
|
+
Support tables are small database tables that contain static data that rarely changes. They are often used to represent enumerations or lookup values in an application and values are often referenced directly from code.
|
|
10
|
+
|
|
11
|
+
This gem provides a simple DSL for defining your Rails models as support tables. When a model is defined as a support table
|
|
12
|
+
|
|
13
|
+
- the data for the table can be defined in a YAML file and distributed with the code
|
|
14
|
+
- helper methods can be generated to allow code to reference specific rows from the table
|
|
15
|
+
- lookups from the table will use caching to avoid querying the database repeatedly for data that rarely changes
|
|
16
|
+
|
|
17
|
+
If you have more advanced needs, you can use the [support_table_data](https://github.com/bdurand/support_table_data) and [support_table_cache](https://github.com/bdurand/support_table_cache) gems directly or in combination with this gem.
|
|
18
|
+
|
|
19
|
+
## Table of Contents
|
|
20
|
+
|
|
21
|
+
- [Usage](#usage)
|
|
22
|
+
- [Defining Support Table Data](#defining-support-table-data)
|
|
23
|
+
- [Advanced Data Settings](#advanced-data-settings)
|
|
24
|
+
- [Key Attribute](#key-attribute)
|
|
25
|
+
- [Data File](#data-file)
|
|
26
|
+
- [Additional Helper Methods](#additional-helper-methods)
|
|
27
|
+
- [Documenting Helper Methods](#documenting-helper-methods)
|
|
28
|
+
- [More Data Options](#more-data-options)
|
|
29
|
+
- [Caching](#caching)
|
|
30
|
+
- [Specifying Additional Cache Keys](#specifying-additional-cache-keys)
|
|
31
|
+
- [Changing The Cache Implementation](#changing-the-cache-implementation)
|
|
32
|
+
- [Cache TTL](#cache-ttl)
|
|
33
|
+
- [Belongs To Caching](#belongs-to-caching)
|
|
34
|
+
- [More Cache Options](#more-cache-options)
|
|
35
|
+
- [Full Example](#full-example)
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
Start by including the `SupportTable` concern in your `ApplicationRecord` base model.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
46
|
+
self.abstract_class = true
|
|
47
|
+
|
|
48
|
+
include SupportTable
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
This will add DSL methods to your models for defining support tables and associations to them.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class Status < ApplicationRecord
|
|
56
|
+
support_table
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Task < ApplicationRecord
|
|
60
|
+
belongs_to_support_table :status
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Defining Support Table Data
|
|
65
|
+
|
|
66
|
+
You can define the data for your support table in the `db/support_tables` directory in a YAML file. The file name should match the model name in plural form. For the `Status` model, the file would be `db/support_tables/statuses.yml`.
|
|
67
|
+
|
|
68
|
+
> [!NOTE]
|
|
69
|
+
> If the model is in a namespace, the directory structure should reflect that (e.g. `Status::Group` data would be defined in the `db/support_tables/status/groups.yml` file).
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
draft:
|
|
73
|
+
id: 1
|
|
74
|
+
name: Draft
|
|
75
|
+
|
|
76
|
+
pending:
|
|
77
|
+
id: 2
|
|
78
|
+
name: Pending
|
|
79
|
+
|
|
80
|
+
in_progress:
|
|
81
|
+
id: 3
|
|
82
|
+
name: In Progress
|
|
83
|
+
|
|
84
|
+
completed:
|
|
85
|
+
id: 4
|
|
86
|
+
name: Completed
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The keys in the YAML file are named instances of the model. These will dynamically generate helper methods to load the instance and to test if a value is that instance.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
Status.draft # returns the Status instance with name "Draft"
|
|
93
|
+
|
|
94
|
+
task.status.draft? # returns true if the task's status is "Draft"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Data will be automatically synced to the database whenever you run any of the following rake tasks:
|
|
98
|
+
|
|
99
|
+
- `db:seed`
|
|
100
|
+
- `db:seed:replant`
|
|
101
|
+
- `db:prepare`
|
|
102
|
+
- `db:test:prepare`
|
|
103
|
+
- `db:fixtures:load`
|
|
104
|
+
|
|
105
|
+
You can also manually trigger synchronization with the `support_table_data:sync` rake task.
|
|
106
|
+
|
|
107
|
+
You **must** popululate the support table data in your test database during your deploy process or test suite setup to ensure that the data is present when your application code runs.
|
|
108
|
+
|
|
109
|
+
> [!TIP]
|
|
110
|
+
> You can also call `SupportTable.sync_all!` in from your application code to synchronize the data.
|
|
111
|
+
|
|
112
|
+
#### Advanced Data Settings
|
|
113
|
+
|
|
114
|
+
You can customize the behavior of the support table data syncing by passing options to the `support_table` method.
|
|
115
|
+
|
|
116
|
+
##### Key Attribute
|
|
117
|
+
|
|
118
|
+
One of the attributes on the table will be used as the unique identifier for each row in the YAML file. By default, this is will be the primary key on the table. You can set a different attribute by passing the `key_attribute` option.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class Status < ApplicationRecord
|
|
122
|
+
support_table key_attribute: :name
|
|
123
|
+
end
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> [!TIP]
|
|
127
|
+
> If you are starting from scratch, the primary key is the best choice for the key attribute. If all of the data in the table will be specified in the YAML file, you should turn off auto incrementing primary keys with `auto_increment: false` on the `create_table` statement.
|
|
128
|
+
>
|
|
129
|
+
> If you are adding support table behavior to an existing table, you may need to use an existing unique attribute instead especially if the primary key is not identical in the existing environments.
|
|
130
|
+
>
|
|
131
|
+
> Make sure that the attribute you choose has a unique index on it to ensure data integrity and that the values will never need to change. Using a display attribute can be problematic if it ever needs to change. It's best to add a new internal identifier attribute if needed (i.e. add a `code` attribute instead of using the `name` attribute if the `name` might ever change).
|
|
132
|
+
|
|
133
|
+
#### Data File
|
|
134
|
+
|
|
135
|
+
You can customize the location of the YAML data file by passing the `data_file` option to the `support_table` method.
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class Status < ApplicationRecord
|
|
139
|
+
support_table data_file: "custom/statuses.yml"
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You can also change the base directory for all support table data files by setting `SupportTable.data_directory` in your application configuration (e.g., in an initializer).
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# config/initializers/support_table.rb
|
|
147
|
+
SupportTable.data_directory = Rails.root.join("db", "support_tables")
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Additional Helper Methods
|
|
151
|
+
|
|
152
|
+
You can add additional helper methods to your support table models by passing `attribute_helpers` to the `support_table` method.
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
class Status < ApplicationRecord
|
|
156
|
+
support_table attribute_helpers: [:name]
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This will add class methods to the class to reference values directly from the YAML file. You automatically get helper methods for the key attribute defined for the table.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
Status.draft_name # returns "Draft"
|
|
164
|
+
Status.pending_name # returns "Pending"
|
|
165
|
+
|
|
166
|
+
Status.draft_id # returns 1
|
|
167
|
+
Status.pending_id # returns 2
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
These helper methods do not need to hit the database and so can be used in situations where the database is not available (i.e. during application initialization).
|
|
171
|
+
|
|
172
|
+
> [!TIP]
|
|
173
|
+
> You use these methods in lieu of hard coding values in the code or defining constants. This keeps all of the support table data defined in one place (the YAML file) and avoids duplication.
|
|
174
|
+
|
|
175
|
+
#### Documenting Helper Methods
|
|
176
|
+
|
|
177
|
+
You can generate YARD documentation for the dynamically generated helper methods by running the rake task `support_table_data:yard_docs:add`. This will add YARD comments to you support table models for the named instance helper methods (i.e. `Status.draft`, etc.). This exposes these methods in your source code for other developers and AI agents to see.
|
|
178
|
+
|
|
179
|
+
You can also run the `support_table_data:yard_docs:verify` rake task in your CI pipeline to ensure that the documentation is up to date. This task will fail if the documentation is missing or out of date.
|
|
180
|
+
|
|
181
|
+
#### More Data Options
|
|
182
|
+
|
|
183
|
+
See the [support_table_data](https://github.com/bdurand/support_table_data) gem for more details on defining support table data.
|
|
184
|
+
|
|
185
|
+
You can use any of the DSL methods defined in that gem to further customize how data is loaded.
|
|
186
|
+
|
|
187
|
+
### Caching
|
|
188
|
+
|
|
189
|
+
Support table data is often read frequently but changes rarely. To improve performance, lookups from support tables by the key attribute are cached by default. Any query that queries a record by the key attribute (i.e. `find_by(name: "Draft")` if `name` is the key attribute) will use the cache. The `id` column is also always cacheable so `find(1)` or `find_by(id: 1)` will also use the cache.
|
|
190
|
+
|
|
191
|
+
You can use the `fetch_by` method to better express in your code that the lookup is using a cache. This method is an alias for `find_by` except that it will raise an error if the lookup is not using a cache on that attribute.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
# Uses the cache or raise an error if the model does not support caching by `name`.
|
|
195
|
+
Status.fetch_by(name: "Draft")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Specifying Additional Cache Keys
|
|
199
|
+
|
|
200
|
+
You can also manually specify the attributes that can be used for caching by passing the `cache_by` option to the `support_table` method.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
class Status < ApplicationRecord
|
|
204
|
+
support_table cache_by: :name # uses both id and name for caching
|
|
205
|
+
end
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
You can even specify composite keys with the `cache_by` method.
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
create_table :statuses do |t|
|
|
212
|
+
t.string :name, null: false
|
|
213
|
+
t.string :group, null: false
|
|
214
|
+
t.index [:name, :group], unique: true
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
class Status < ApplicationRecord
|
|
218
|
+
support_table
|
|
219
|
+
cache_by [:name, :group]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
Status.fetch_by(name: "Draft", group: "Non-Live") # Uses the cache
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### Changing The Cache Implementation
|
|
226
|
+
|
|
227
|
+
The default caching implementation uses an in memory cache that stores rows in local memory for the lifetime of the application process. This is the most efficient caching strategy for most use cases. If you need a different caching strategy, you can customize it by passing an implementation of `ActiveSupport::Cache::Store` to the `cache` option of the `support_table` method.
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
class Status < ApplicationRecord
|
|
231
|
+
# Use the application cache for caching.
|
|
232
|
+
support_table cache: Rails.cache
|
|
233
|
+
end
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
You can also disable caching entirely by passing `cache: false` or `cache: nil`.
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
class Status < ApplicationRecord
|
|
240
|
+
# Disable caching.
|
|
241
|
+
support_table cache: false
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
You can specify the value `:memory` to use an in memory cache. This is the default behavior.
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
class Status < ApplicationRecord
|
|
249
|
+
# Use an in memory cache.
|
|
250
|
+
support_table cache: :memory
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
#### Cache TTL
|
|
255
|
+
|
|
256
|
+
By default, cached records are stored indefinitely (until the application process restarts or the cache is cleared). You can set a time-to-live (TTL) for cached records by passing the `ttl` option to the `support_table` method.
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class Status < ApplicationRecord
|
|
260
|
+
# Cache records for 1 hour.
|
|
261
|
+
support_table ttl: 1.hour
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The TTL value should be a number of seconds or an `ActiveSupport::Duration` object. When a cached record expires, it will be reloaded from the database on the next access.
|
|
266
|
+
|
|
267
|
+
> [!TIP]
|
|
268
|
+
> For most support tables, you don't need to set a TTL since the data rarely changes. An in-memory cache without a TTL is the most performant option. Only set a TTL if you need to ensure the data is refreshed periodically.
|
|
269
|
+
|
|
270
|
+
#### Belongs To Caching
|
|
271
|
+
|
|
272
|
+
When you have another model with a `belongs_to` association to a support table, you can use the `belongs_to_support_table` method to define the association. This will ensure that lookups for the associated support table record will use the cache.
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
class Task < ApplicationRecord
|
|
276
|
+
belongs_to_support_table :status
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This method works exactly like the standard `belongs_to` method except that it ensures that lookups for the associated support table record will use the cache.
|
|
281
|
+
|
|
282
|
+
#### More Cache Options
|
|
283
|
+
|
|
284
|
+
See the [support_table_cache](https://github.com/bdurand/support_table_cache) gem for more details on caching support table data.
|
|
285
|
+
|
|
286
|
+
You can use any of the DSL methods defined in that gem to further customize how models are cached.
|
|
287
|
+
|
|
288
|
+
### Full Example
|
|
289
|
+
|
|
290
|
+
For this example, we'll start with a simple application for managing a list of tasks. Each task will have a status.
|
|
291
|
+
|
|
292
|
+
First, let's start with the table definitions.
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
create_table :statuses do |t|
|
|
296
|
+
t.string :name, null: false, index: {unique: true}
|
|
297
|
+
t.timestamps
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
create_table :tasks do |t|
|
|
301
|
+
t.integer :status_id, null: false, index: true
|
|
302
|
+
t.string :description, null: false
|
|
303
|
+
t.timestamps
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
First, you need to include the `SupportTable` concern in your `ApplicationRecord` so that all models have access to it.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
311
|
+
self.abstract_class = true
|
|
312
|
+
|
|
313
|
+
include SupportTable
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
This will add DSL methods to your models for defining support tables and associations to them.
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
class Status < ApplicationRecord
|
|
321
|
+
support_table
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class Task < ApplicationRecord
|
|
325
|
+
belongs_to_support_table :status
|
|
326
|
+
end
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Next add the YAML file at `db/support_tables/statuses.yml` to define the data that should be in the `statuses` table.
|
|
330
|
+
|
|
331
|
+
```yaml
|
|
332
|
+
draft:
|
|
333
|
+
id: 1
|
|
334
|
+
name: Draft
|
|
335
|
+
|
|
336
|
+
pending:
|
|
337
|
+
id: 2
|
|
338
|
+
name: Pending
|
|
339
|
+
|
|
340
|
+
in_progress:
|
|
341
|
+
id: 3
|
|
342
|
+
name: In Progress
|
|
343
|
+
|
|
344
|
+
finished:
|
|
345
|
+
id: 4
|
|
346
|
+
name: Finished
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
That's it. Now the `Status` will load data from a YAML file and cache lookups automatically including from the `Task#status` association.
|
|
350
|
+
|
|
351
|
+
## Installation
|
|
352
|
+
|
|
353
|
+
Add this line to your application's Gemfile:
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
gem "support_table"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Then execute:
|
|
360
|
+
```bash
|
|
361
|
+
$ bundle
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Or install it yourself as:
|
|
365
|
+
```bash
|
|
366
|
+
$ gem install support_table
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Contributing
|
|
370
|
+
|
|
371
|
+
Open a pull request on [GitHub](https://github.com/bdurand/support_table).
|
|
372
|
+
|
|
373
|
+
Please use the [standardrb](https://github.com/testdouble/standard) syntax and lint your code with `standardrb --fix` before submitting.
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupportTable
|
|
4
|
+
# Module to add support for specifying `belongs_to` associations that
|
|
5
|
+
# reference support tables.
|
|
6
|
+
module BelongsToSupportTable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
# Define a `belongs_to` association that references a support table. This method takes
|
|
11
|
+
# the same arguments as `belongs_to`. It will add a caching layer to the association
|
|
12
|
+
# to minimize database queries.
|
|
13
|
+
#
|
|
14
|
+
# @param name [Symbol] The name of the association.
|
|
15
|
+
# @param args [Array] Additional arguments to pass to `belongs_to`.
|
|
16
|
+
# @param kwargs [Hash] Additional keyword arguments to pass to `belongs_to`.
|
|
17
|
+
# @return [void]
|
|
18
|
+
def belongs_to_support_table(name, *args, **kwargs)
|
|
19
|
+
unless include?(SupportTableCache::Associations)
|
|
20
|
+
include SupportTableCache::Associations
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
retval = belongs_to name, *args, **kwargs
|
|
24
|
+
cache_belongs_to name
|
|
25
|
+
retval
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_leteral: true
|
|
2
|
+
|
|
3
|
+
module SupportTable
|
|
4
|
+
# Class to define support table behavior on an ActiveRecord class.
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
class Definition
|
|
8
|
+
attr_reader :klass
|
|
9
|
+
|
|
10
|
+
def initialize(klass)
|
|
11
|
+
@klass = klass
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Setup a class to be a support table.
|
|
15
|
+
#
|
|
16
|
+
# @param data_file [String, Array<String>, nil] Path to data files to load the table from.
|
|
17
|
+
# @param key_attribute [String, Symbol, nil] The name of the attribute in the data file that
|
|
18
|
+
# uniquely identifies each row in the data.
|
|
19
|
+
# @param cache_by [String, Symbol, Array, nil] List of attributes that can be used to uniquely
|
|
20
|
+
# identify a row that can be used for caching records.
|
|
21
|
+
# @param cache [ActiveSupport::Cache::Store, Symbol, Boolean, nil] The caching mechanism to use.
|
|
22
|
+
# @param ttl [Numeric, ActiveSupport::Duration, nil] The time-to-live (in seconds) for cached records.
|
|
23
|
+
# @return [void]
|
|
24
|
+
def support_table(data_file:, key_attribute:, attribute_helpers:, cache_by:, cache:, ttl:)
|
|
25
|
+
include_modules
|
|
26
|
+
set_key_attribute(key_attribute)
|
|
27
|
+
set_attribute_helpers(attribute_helpers)
|
|
28
|
+
set_data_file(data_file)
|
|
29
|
+
setup_caching(cache_by, cache, ttl)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def include_modules
|
|
35
|
+
klass.include SupportTableData unless klass.include?(SupportTableData)
|
|
36
|
+
klass.include SupportTableCache unless klass.include?(SupportTableCache)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_key_attribute(key_attribute)
|
|
40
|
+
klass.support_table_key_attribute = key_attribute if key_attribute
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def set_attribute_helpers(attribute_helpers)
|
|
44
|
+
helpers = [klass.support_table_key_attribute]
|
|
45
|
+
helpers.concat(Array(attribute_helpers)) if attribute_helpers
|
|
46
|
+
klass.named_instance_attribute_helpers(*helpers.uniq.compact)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_data_file(data_file)
|
|
50
|
+
if data_file
|
|
51
|
+
Array(data_file).each do |file|
|
|
52
|
+
klass.add_support_table_data(file)
|
|
53
|
+
end
|
|
54
|
+
elsif data_file.nil?
|
|
55
|
+
data_root_dir = klass.support_table_data_directory || SupportTableData.data_directory || Dir.pwd
|
|
56
|
+
default_data_file = File.join(data_root_dir, "#{klass.name.pluralize.underscore}.yml")
|
|
57
|
+
if File.exist?(default_data_file)
|
|
58
|
+
klass.add_support_table_data(default_data_file)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def setup_caching(cache_by, cache, ttl)
|
|
64
|
+
if cache == false
|
|
65
|
+
klass.send(:cache_by, false)
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
cache_keys = [klass.support_table_key_attribute, "id"]
|
|
70
|
+
cache_keys.concat(Array(cache_by)) if cache_by
|
|
71
|
+
cache_keys.uniq! { |key| Array(key).collect(&:to_s) }
|
|
72
|
+
|
|
73
|
+
cache_keys.each do |key|
|
|
74
|
+
klass.send(:cache_by, key)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
klass.support_table_cache = cache
|
|
78
|
+
klass.support_table_cache_ttl = ttl
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "support_table_cache"
|
|
4
|
+
require "support_table_data"
|
|
5
|
+
|
|
6
|
+
# Include this module to add the ability to define support tables in ActiveRecord models.
|
|
7
|
+
# This will add the `support_table` and `belongs_to_support_table` class methods to the model.
|
|
8
|
+
#
|
|
9
|
+
# A support table is a table that contains a fixed set of data that is used to support
|
|
10
|
+
# the application. This data is typically loaded from a data file (YAML, JSON, or CSV)
|
|
11
|
+
# and is intended to be read-only. Support tables are also usually small and can be
|
|
12
|
+
# cached in memory for fast access.
|
|
13
|
+
#
|
|
14
|
+
# You would normally just include this module in your `ApplicationRecord` class. However,
|
|
15
|
+
# you can also include it in individual models.
|
|
16
|
+
#
|
|
17
|
+
# @example Define a support table
|
|
18
|
+
#
|
|
19
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
20
|
+
# self.abstract_class = true
|
|
21
|
+
#
|
|
22
|
+
# include SupportTable
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# class Status < ApplicationRecord
|
|
26
|
+
# support_table
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# class Task < ApplicationRecord
|
|
30
|
+
# belongs_to_support_table :status
|
|
31
|
+
# end
|
|
32
|
+
module SupportTable
|
|
33
|
+
extend ActiveSupport::Concern
|
|
34
|
+
|
|
35
|
+
VERSION = File.read(File.join(__dir__, "../VERSION")).strip
|
|
36
|
+
|
|
37
|
+
included do
|
|
38
|
+
include BelongsToSupportTable unless include?(BelongsToSupportTable)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module ClassMethods
|
|
42
|
+
# Define details about the support table. You only need to call this method if
|
|
43
|
+
# you need to override any of the defaults. See SupportTableData and SupportTableCache
|
|
44
|
+
# for more details.
|
|
45
|
+
#
|
|
46
|
+
# @param data_file [String, Array<String>, nil] Path to the data file to use to load the table. This
|
|
47
|
+
# should be a YAML, JSON, or CSV file that defines the records that should always exist in
|
|
48
|
+
# the table. If no value is specified, then a YAML file in the data directory with the
|
|
49
|
+
# same name as the underscored, pluralized name of the class will be used. For example,
|
|
50
|
+
# if the class name is `Task::Status`, then it will look for the file `task/statuses.yml`
|
|
51
|
+
# in the `db/support_tables` directory.
|
|
52
|
+
#
|
|
53
|
+
# @param key_attribute [String, Symbol, nil] The name of the attribute in the data file that
|
|
54
|
+
# uniquely identifies each row in the data. By default this will be the primary key
|
|
55
|
+
# attribute of the table (usually `id`).
|
|
56
|
+
#
|
|
57
|
+
# @param attribute_helpers [String, Symbol, Array<String, Symbol>, nil] List of attributes
|
|
58
|
+
# which should have helper methods created for them. This generates methods for each named instance
|
|
59
|
+
# to get that attribute directly from the YAML data. For example, adding a helper for `:name`
|
|
60
|
+
# will create a class method `*_name` for each named instance (i.e. Status.draft_name).
|
|
61
|
+
#
|
|
62
|
+
# @param cache_by [String, Symbol, Array, nil] List of attributes that can be used to uniquely
|
|
63
|
+
# identify a row that can be used for caching records. If a unique key is made up of
|
|
64
|
+
# multiple columns, then you will need to set it up with a call to `SupportTableCache.cache_by`
|
|
65
|
+
#
|
|
66
|
+
# @param cache [ActiveSupport::Cache::Store, Symbol, Boolean, nil] The caching mechanism to use.
|
|
67
|
+
# This can be either an instance of `ActiveSupport::Cache::Store` like `Rails.cache`, or
|
|
68
|
+
# the value `:memory` to use an in-memory cache, or `false` to disable caching.
|
|
69
|
+
#
|
|
70
|
+
#
|
|
71
|
+
# @param ttl [Numeric, ActiveSupport::Duration, nil] The time-to-live (in seconds) for cached records. If not specified,
|
|
72
|
+
# @return [void]
|
|
73
|
+
def support_table(data_file: nil, key_attribute: nil, attribute_helpers: nil, cache_by: nil, cache: :memory, ttl: nil)
|
|
74
|
+
SupportTable::Definition.new(self).support_table(
|
|
75
|
+
data_file: data_file,
|
|
76
|
+
key_attribute: key_attribute,
|
|
77
|
+
attribute_helpers: attribute_helpers,
|
|
78
|
+
cache_by: cache_by,
|
|
79
|
+
cache: cache,
|
|
80
|
+
ttl: ttl
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class << self
|
|
86
|
+
# Set the directory where data files are stored. See SupportTableData for details.
|
|
87
|
+
# This is an alias for `SupportTableData.data_directory=`.
|
|
88
|
+
#
|
|
89
|
+
# @param dir [String] Path to the directory where data files are stored.
|
|
90
|
+
# @return [void]
|
|
91
|
+
def data_directory=(dir)
|
|
92
|
+
SupportTableData.data_directory = dir
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Sync all support tables. See SupportTableData for details. This is an alias for
|
|
96
|
+
# `SupportTableData.sync_all!`.
|
|
97
|
+
#
|
|
98
|
+
# @param classes [Array<Class>] List of support table classes to sync. If no classes
|
|
99
|
+
# are specified, then all support tables will be synced.
|
|
100
|
+
# @return [void]
|
|
101
|
+
def sync_all!(*classes)
|
|
102
|
+
SupportTableData.sync_all!(*classes)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Set the cache used on support tables. See SupportTableCache for details. This is an
|
|
106
|
+
# alias for `SupportTableCache.cache=`.
|
|
107
|
+
#
|
|
108
|
+
# @param cache_impl [ActiveSupport::Cache::Store] The cache implementation to use.
|
|
109
|
+
# @return [void]
|
|
110
|
+
def cache=(cache_impl)
|
|
111
|
+
SupportTableCache.cache = cache_impl
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
require_relative "support_table/belongs_to_support_table"
|
|
117
|
+
require_relative "support_table/definition"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Gem::Specification.new do |spec|
|
|
2
|
+
spec.name = "support_table"
|
|
3
|
+
spec.version = File.read(File.expand_path("../VERSION", __FILE__)).strip
|
|
4
|
+
spec.authors = ["Brian Durand"]
|
|
5
|
+
spec.email = ["bbdurand@gmail.com"]
|
|
6
|
+
|
|
7
|
+
spec.summary = "Support tables for ActiveRecord models with YAML-backed data, helper methods, and automatic caching for small tables."
|
|
8
|
+
|
|
9
|
+
spec.homepage = "https://github.com/bdurand/support_table"
|
|
10
|
+
spec.license = "MIT"
|
|
11
|
+
|
|
12
|
+
spec.metadata = {
|
|
13
|
+
"homepage_uri" => spec.homepage,
|
|
14
|
+
"source_code_uri" => spec.homepage,
|
|
15
|
+
"changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
20
|
+
ignore_files = %w[
|
|
21
|
+
.
|
|
22
|
+
AGENTS.md
|
|
23
|
+
Appraisals
|
|
24
|
+
Gemfile
|
|
25
|
+
Gemfile.lock
|
|
26
|
+
Rakefile
|
|
27
|
+
bin/
|
|
28
|
+
gemfiles/
|
|
29
|
+
spec/
|
|
30
|
+
test_app/
|
|
31
|
+
]
|
|
32
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
|
33
|
+
`git ls-files -z`.split("\x0").reject { |f| ignore_files.any? { |path| f.start_with?(path) } }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
spec.require_paths = ["lib"]
|
|
37
|
+
|
|
38
|
+
spec.add_dependency "support_table_cache", "~> 1.1", ">= 1.1.5"
|
|
39
|
+
spec.add_dependency "support_table_data", "~> 1.5", ">= 1.5.0"
|
|
40
|
+
|
|
41
|
+
spec.add_development_dependency "bundler"
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: support_table
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Brian Durand
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: support_table_cache
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 1.1.5
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '1.1'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 1.1.5
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: support_table_data
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.5'
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: 1.5.0
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - "~>"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.5'
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: 1.5.0
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: bundler
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
type: :development
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
email:
|
|
67
|
+
- bbdurand@gmail.com
|
|
68
|
+
executables: []
|
|
69
|
+
extensions: []
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
files:
|
|
72
|
+
- CHANGELOG.md
|
|
73
|
+
- MIT-LICENSE
|
|
74
|
+
- README.md
|
|
75
|
+
- VERSION
|
|
76
|
+
- lib/support_table.rb
|
|
77
|
+
- lib/support_table/belongs_to_support_table.rb
|
|
78
|
+
- lib/support_table/definition.rb
|
|
79
|
+
- support_table.gemspec
|
|
80
|
+
homepage: https://github.com/bdurand/support_table
|
|
81
|
+
licenses:
|
|
82
|
+
- MIT
|
|
83
|
+
metadata:
|
|
84
|
+
homepage_uri: https://github.com/bdurand/support_table
|
|
85
|
+
source_code_uri: https://github.com/bdurand/support_table
|
|
86
|
+
changelog_uri: https://github.com/bdurand/support_table/blob/main/CHANGELOG.md
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 4.0.3
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: Support tables for ActiveRecord models with YAML-backed data, helper methods,
|
|
104
|
+
and automatic caching for small tables.
|
|
105
|
+
test_files: []
|