support_table_data 1.4.0 → 1.5.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 +4 -4
- data/AGENTS.md +140 -0
- data/ARCHITECTURE.md +538 -0
- data/CHANGELOG.md +13 -0
- data/README.md +50 -5
- data/VERSION +1 -1
- data/lib/support_table_data/documentation/source_file.rb +95 -0
- data/lib/support_table_data/documentation/yard_doc.rb +91 -0
- data/lib/support_table_data/documentation.rb +9 -0
- data/lib/support_table_data/railtie.rb +22 -1
- data/lib/support_table_data/validation_error.rb +16 -0
- data/lib/support_table_data.rb +71 -53
- data/lib/tasks/support_table_data.rake +55 -12
- data/lib/tasks/utils.rb +63 -0
- data/support_table_data.gemspec +1 -1
- data/test_app/.gitignore +4 -0
- data/test_app/Gemfile +7 -0
- data/test_app/Rakefile +6 -0
- data/test_app/app/models/application_record.rb +5 -0
- data/test_app/app/models/secondary_application_record.rb +7 -0
- data/test_app/app/models/status.rb +11 -0
- data/test_app/app/models/thing.rb +10 -0
- data/test_app/bin/rails +4 -0
- data/test_app/config/application.rb +42 -0
- data/test_app/config/boot.rb +3 -0
- data/test_app/config/database.yml +17 -0
- data/test_app/config/environment.rb +5 -0
- data/test_app/config/environments/development.rb +11 -0
- data/test_app/config/environments/test.rb +11 -0
- data/test_app/config.ru +6 -0
- data/test_app/db/migrate/20260103060951_create_status.rb +8 -0
- data/test_app/db/schema.rb +20 -0
- data/test_app/db/secondary_migrate/20260104000001_create_things.rb +7 -0
- data/test_app/db/secondary_schema.rb +25 -0
- data/test_app/db/support_tables/statuses.yml +19 -0
- data/test_app/db/support_tables/things.yml +5 -0
- data/test_app/lib/tasks/database.rake +11 -0
- data/test_app/log/.keep +0 -0
- metadata +34 -8
data/README.md
CHANGED
|
@@ -8,6 +8,19 @@ This gem provides a mixin for ActiveRecord support table models that allows you
|
|
|
8
8
|
|
|
9
9
|
These kinds of models blur the line between data and code. You'll often end up with constants and application logic based on specific values that need to exist in the table. By using this gem, you can easily define methods for loading and comparing specific instances. This can give you cleaner code that reads far more naturally. You can also avoid defining dozens of constants or referencing magic values (i.e. no more hard-coded strings or ids in the code to look up specific records).
|
|
10
10
|
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
- [Specifying Data Files](#specifying-data-files)
|
|
15
|
+
- [Named Instances](#named-instances)
|
|
16
|
+
- [Documenting Named Instance Helpers](#documenting-named-instance-helpers)
|
|
17
|
+
- [Caching](#caching)
|
|
18
|
+
- [Loading Data](#loading-data)
|
|
19
|
+
- [Testing](#testing)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [Contributing](#contributing)
|
|
22
|
+
- [License](#license)
|
|
23
|
+
|
|
11
24
|
## Usage
|
|
12
25
|
|
|
13
26
|
In the examples below, suppose we have a simple `Status` model in which each row has an id and a name, and the name can only have a handful of statuses: "Pending", "In Progress", and "Completed".
|
|
@@ -55,7 +68,9 @@ class Status < ApplicationRecord
|
|
|
55
68
|
|
|
56
69
|
You cannot update the value of the key attribute in a record in the data file. If you do, a new record will be created and the existing record will be left unchanged.
|
|
57
70
|
|
|
58
|
-
You can specify data files as relative paths. This can be done by setting the `SupportTableData.data_directory` value. You can override this value for a model by setting the `support_table_data_directory` attribute on its class.
|
|
71
|
+
You can specify data files as relative paths. This can be done by setting the `SupportTableData.data_directory` value. You can override this value for a model by setting the `support_table_data_directory` attribute on its class. Otherwise, relative file paths will be resolved from the current working directory. You must define the directory to load relative files from before loading your model classes.
|
|
72
|
+
|
|
73
|
+
In a Rails application, `SupportTableData.data_directory` will be automatically set to `db/support_tables/`. This can be overridden by setting the `config.support_table.data_directory` option in the Rails application configuration.
|
|
59
74
|
|
|
60
75
|
**Note**: If you're using CSV files and Ruby 3.4 or higher, you'll need to include the `csv` gem in your Gemfile since it was removed from the standard library in Ruby 3.4.
|
|
61
76
|
|
|
@@ -109,7 +124,6 @@ Helper methods will not override already defined methods on a model class. If a
|
|
|
109
124
|
|
|
110
125
|
You can also define helper methods for named instance attributes. These helper methods will return the hard coded values from the data file. Calling these methods does not require a database connection.
|
|
111
126
|
|
|
112
|
-
|
|
113
127
|
```ruby
|
|
114
128
|
class Status < ApplicationRecord
|
|
115
129
|
include SupportTableData
|
|
@@ -171,6 +185,19 @@ completed:
|
|
|
171
185
|
group_name: done
|
|
172
186
|
```
|
|
173
187
|
|
|
188
|
+
#### Documenting Named Instance Helpers
|
|
189
|
+
|
|
190
|
+
In a Rails application, you can add YARD documentation for the named instance helpers by running the rake task `support_table_data:yard_docs:add`. This will add YARD comments to your model classes for each of the named instance helper methods defined on the model. Adding this documentation will help IDEs provide better code completion and inline documentation for the helper methods and expose the methods to AI agents.
|
|
191
|
+
|
|
192
|
+
The default behavior is to add the documentation comments at the end of the model class by reopening the class definition. If you prefer to have the documentation comments appear elsewhere in the file, you can add the following markers to your model class and the YARD documentation will be inserted between these markers.
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
# Begin YARD docs for support_table_data
|
|
196
|
+
# End YARD docs for support_table_data
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A good practice is to add a check to your CI pipeline to ensure the documentation is always up to date. You can run the rake task `support_table_data:yard_docs:verify` to do this. It will exit with an error if any models do not have up to date documentation.
|
|
200
|
+
|
|
174
201
|
### Caching
|
|
175
202
|
|
|
176
203
|
You can use the companion [support_table_cache gem](https://github.com/bdurand/support_table_cache) to add caching support to your models. That way your application won't need to constantly query the database for records that will never change.
|
|
@@ -198,6 +225,9 @@ class Thing < ApplicationRecord
|
|
|
198
225
|
end
|
|
199
226
|
```
|
|
200
227
|
|
|
228
|
+
> [!TIP]
|
|
229
|
+
> The [support_table](https://github.com/bdurand/support_table) gem combines both gems in a drop in solution for Rails applications.
|
|
230
|
+
|
|
201
231
|
### Loading Data
|
|
202
232
|
|
|
203
233
|
Calling `sync_table_data!` on your model class will synchronize the data in the database table with the values from the data files.
|
|
@@ -246,18 +276,33 @@ end
|
|
|
246
276
|
|
|
247
277
|
If you use a method to set a `has_many` association on your model, you **must** set the `autosave` option to `true` on the association (see the above example). This will ensure the association records are always saved even if there were no changes to the parent record.
|
|
248
278
|
|
|
249
|
-
You need to call `SupportTableData.sync_all!` when deploying your application. This gem includes a rake task `support_table_data:sync` that is suitable for hooking into deploy
|
|
279
|
+
You will need to call `SupportTableData.sync_all!` when deploying your application or running your test suite. This gem includes a rake task `support_table_data:sync` that is suitable for hooking into deploy or CI scripts.
|
|
280
|
+
|
|
281
|
+
This task is automatically run whenever you run any of these Rails tasks so if these are already part of your deploy or CI scripts, then no additional setup is required:
|
|
282
|
+
|
|
283
|
+
- `db:seed`
|
|
284
|
+
- `db:seed:replant`
|
|
285
|
+
- `db:prepare`
|
|
286
|
+
- `db:test:prepare`
|
|
287
|
+
- `db:fixtures:load`
|
|
288
|
+
|
|
289
|
+
You can disable these task enhancements by setting `config.support_table.auto_sync = false` in your Rails application configuration.
|
|
290
|
+
|
|
291
|
+
> [!TIP]
|
|
292
|
+
> If you also want to hook into the `db:migrate` task so that syncs are run immediately after database migrations, you can do this by adding code to a Rakefile in your application's `lib/tasks` directory. Migrations do funny things with the database connection especially when using multiple databases so you need to re-establish the connection before syncing the support table data.
|
|
250
293
|
|
|
251
294
|
```ruby
|
|
252
295
|
if Rake::Task.task_defined?("db:migrate")
|
|
253
296
|
Rake::Task["db:migrate"].enhance do
|
|
297
|
+
# The main database connection may have artifacts from the migration, so re-establish it
|
|
298
|
+
# to get a clean connection before syncing support table data.
|
|
299
|
+
ActiveRecord::Base.establish_connection
|
|
300
|
+
|
|
254
301
|
Rake::Task["support_table_data:sync"].invoke
|
|
255
302
|
end
|
|
256
303
|
end
|
|
257
304
|
```
|
|
258
305
|
|
|
259
|
-
Enhancing the `db:migrate` task also ensures that local development environments will stay up to date.
|
|
260
|
-
|
|
261
306
|
### Testing
|
|
262
307
|
|
|
263
308
|
You must also call `SupportTableData.sync_all!` before running your test suite. This method should be called in the test suite setup code after any data in the test database has been purged and before any tests are run.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.5.0
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupportTableData
|
|
4
|
+
module Documentation
|
|
5
|
+
class SourceFile
|
|
6
|
+
attr_reader :klass, :path
|
|
7
|
+
|
|
8
|
+
BEGIN_YARD_COMMENT = "# Begin YARD docs for support_table_data"
|
|
9
|
+
END_YARD_COMMENT = "# End YARD docs for support_table_data"
|
|
10
|
+
YARD_COMMENT_REGEX = /^(?<indent>[ \t]*)#{BEGIN_YARD_COMMENT}.*^[ \t]*#{END_YARD_COMMENT}$/m
|
|
11
|
+
CLASS_DEF_REGEX = /^[ \t]*class [a-zA-Z_0-9:]+.*?$/
|
|
12
|
+
|
|
13
|
+
# Initialize a new source file representation.
|
|
14
|
+
#
|
|
15
|
+
# @param klass [Class] The model class
|
|
16
|
+
# @param path [Pathname] The path to the source file
|
|
17
|
+
def initialize(klass, path)
|
|
18
|
+
@klass = klass
|
|
19
|
+
@path = path
|
|
20
|
+
@source = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Return the source code of the file.
|
|
24
|
+
#
|
|
25
|
+
# @return [String]
|
|
26
|
+
def source
|
|
27
|
+
@source ||= @path.read
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the source code without any generated YARD documentation.
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
def source_without_yard_docs
|
|
34
|
+
"#{source.sub(YARD_COMMENT_REGEX, "").rstrip}#{trailing_newline}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return the source code with the generated YARD documentation added.
|
|
38
|
+
# The YARD docs are identified by a begin and end comment block. By default
|
|
39
|
+
# the generated docs are added to the end of the file by reopening the class
|
|
40
|
+
# definition. You can move the comment block inside the original class
|
|
41
|
+
# if desired.
|
|
42
|
+
#
|
|
43
|
+
# @return [String]
|
|
44
|
+
def source_with_yard_docs
|
|
45
|
+
yard_docs = YardDoc.new(klass).named_instance_yard_docs
|
|
46
|
+
return source if yard_docs.nil?
|
|
47
|
+
|
|
48
|
+
existing_yard_docs = source.match(YARD_COMMENT_REGEX)
|
|
49
|
+
if existing_yard_docs
|
|
50
|
+
indent = existing_yard_docs[:indent]
|
|
51
|
+
has_class_def = existing_yard_docs.to_s.match?(CLASS_DEF_REGEX)
|
|
52
|
+
yard_docs = yard_docs.lines.map { |line| line.blank? ? "\n" : "#{indent}#{" " if has_class_def}#{line}" }.join
|
|
53
|
+
|
|
54
|
+
updated_source = source[0, existing_yard_docs.begin(0)]
|
|
55
|
+
updated_source << "#{indent}#{BEGIN_YARD_COMMENT}\n"
|
|
56
|
+
updated_source << "#{indent}class #{klass.name}\n" if has_class_def
|
|
57
|
+
updated_source << yard_docs
|
|
58
|
+
updated_source << "\n#{indent}end" if has_class_def
|
|
59
|
+
updated_source << "\n#{indent}#{END_YARD_COMMENT}"
|
|
60
|
+
updated_source << source[existing_yard_docs.end(0)..-1]
|
|
61
|
+
updated_source
|
|
62
|
+
else
|
|
63
|
+
yard_comments = <<~SOURCE.chomp("\n")
|
|
64
|
+
#{BEGIN_YARD_COMMENT}
|
|
65
|
+
class #{klass.name}
|
|
66
|
+
#{yard_docs.lines.map { |line| line.blank? ? "\n" : " #{line}" }.join}
|
|
67
|
+
end
|
|
68
|
+
#{END_YARD_COMMENT}
|
|
69
|
+
SOURCE
|
|
70
|
+
"#{source.rstrip}\n\n#{yard_comments}#{trailing_newline}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if the YARD documentation in the source file is up to date.
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def yard_docs_up_to_date?
|
|
78
|
+
source == source_with_yard_docs
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if the source file has any YARD documentation added by support_table_data.
|
|
82
|
+
#
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def has_yard_docs?
|
|
85
|
+
source.match?(YARD_COMMENT_REGEX)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def trailing_newline
|
|
91
|
+
source.end_with?("\n") ? "\n" : ""
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupportTableData
|
|
4
|
+
module Documentation
|
|
5
|
+
class YardDoc
|
|
6
|
+
# @param klass [Class] The model class to generate documentation for
|
|
7
|
+
def initialize(klass)
|
|
8
|
+
@klass = klass
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Generate YARD documentation class definition for the model's helper methods.
|
|
12
|
+
#
|
|
13
|
+
# @return [String, nil] The YARD documentation class definition, or nil if no named instances
|
|
14
|
+
def named_instance_yard_docs
|
|
15
|
+
instance_names = klass.instance_names
|
|
16
|
+
generate_yard_docs(instance_names)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Generate YARD documentation comment for named instance singleton method.
|
|
20
|
+
#
|
|
21
|
+
# @param name [String] The name of the instance method.
|
|
22
|
+
# @return [String] The YARD comment text
|
|
23
|
+
def instance_helper_yard_doc(name)
|
|
24
|
+
<<~YARD.chomp("\n")
|
|
25
|
+
# Find the named instance +#{name}+ from the database.
|
|
26
|
+
#
|
|
27
|
+
# @!method self.#{name}
|
|
28
|
+
# @return [#{klass.name}]
|
|
29
|
+
# @raise [ActiveRecord::RecordNotFound] if the record does not exist
|
|
30
|
+
# @!visibility public
|
|
31
|
+
YARD
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Generate YARD documentation comment for the predicate method for the named instance.
|
|
35
|
+
#
|
|
36
|
+
# @param name [String] The name of the instance method.
|
|
37
|
+
# @return [String] The YARD comment text
|
|
38
|
+
def predicate_helper_yard_doc(name)
|
|
39
|
+
<<~YARD.chomp("\n")
|
|
40
|
+
# Check if this record is the named instance +#{name}+.
|
|
41
|
+
#
|
|
42
|
+
# @!method #{name}?
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
# @!visibility public
|
|
45
|
+
YARD
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Generate YARD documentation comment for the attribute method helper for the named instance.
|
|
49
|
+
#
|
|
50
|
+
# @param name [String] The name of the instance method.
|
|
51
|
+
# @return [String] The YARD comment text
|
|
52
|
+
def attribute_helper_yard_doc(name, attribute_name)
|
|
53
|
+
<<~YARD.chomp("\n")
|
|
54
|
+
# Get the #{attribute_name} attribute from the data file
|
|
55
|
+
# for the named instance +#{name}+.
|
|
56
|
+
#
|
|
57
|
+
# @!method self.#{name}_#{attribute_name}
|
|
58
|
+
# @return [Object]
|
|
59
|
+
# @!visibility public
|
|
60
|
+
YARD
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
attr_reader :klass
|
|
66
|
+
|
|
67
|
+
def generate_yard_docs(instance_names)
|
|
68
|
+
return nil if instance_names.empty?
|
|
69
|
+
|
|
70
|
+
yard_lines = ["# @!group Named Instances"]
|
|
71
|
+
|
|
72
|
+
# Generate docs for each named instance
|
|
73
|
+
instance_names.sort.each do |name|
|
|
74
|
+
yard_lines << ""
|
|
75
|
+
yard_lines << instance_helper_yard_doc(name)
|
|
76
|
+
yard_lines << ""
|
|
77
|
+
yard_lines << predicate_helper_yard_doc(name)
|
|
78
|
+
klass.support_table_attribute_helpers.each do |attribute_name|
|
|
79
|
+
yard_lines << ""
|
|
80
|
+
yard_lines << attribute_helper_yard_doc(name, attribute_name)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
yard_lines << ""
|
|
85
|
+
yard_lines << "# @!endgroup"
|
|
86
|
+
|
|
87
|
+
yard_lines.join("\n")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -2,8 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module SupportTableData
|
|
4
4
|
class Railtie < Rails::Railtie
|
|
5
|
-
|
|
5
|
+
unless config.respond_to?(:support_table) && config.support_table
|
|
6
|
+
config.support_table = ActiveSupport::OrderedOptions.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
config.support_table.data_directory ||= "db/support_tables"
|
|
10
|
+
config.support_table.auto_sync ||= true
|
|
11
|
+
|
|
12
|
+
initializer "support_table_data" do |app|
|
|
13
|
+
SupportTableData.data_directory ||= app.root.join(app.config.support_table&.data_directory).to_s
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
rake_tasks do |app|
|
|
6
17
|
load File.expand_path("../tasks/support_table_data.rake", __dir__)
|
|
18
|
+
|
|
19
|
+
if app.config.support_table.auto_sync
|
|
20
|
+
["db:seed", "db:seed:replant", "db:prepare", "db:test:prepare", "db:fixtures:load"].each do |task_name|
|
|
21
|
+
next unless Rake::Task.task_defined?(task_name)
|
|
22
|
+
|
|
23
|
+
Rake::Task[task_name].enhance do
|
|
24
|
+
Rake::Task["support_table_data:sync"].invoke
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
7
28
|
end
|
|
8
29
|
end
|
|
9
30
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupportTableData
|
|
4
|
+
# Error class that is raised when validation fails when loading support table data.
|
|
5
|
+
# It provides more context than the standard ActiveRecord::RecordInvalid to help identify
|
|
6
|
+
# which record caused the validation failure.
|
|
7
|
+
class ValidationError < StandardError
|
|
8
|
+
def initialize(invalid_record)
|
|
9
|
+
key_attribute = invalid_record.class.support_table_key_attribute
|
|
10
|
+
key_value = invalid_record[key_attribute]
|
|
11
|
+
message = "Validation failed for #{invalid_record.class} with #{key_attribute}: #{key_value.inspect} - " \
|
|
12
|
+
"#{invalid_record.errors.full_messages.join(", ")}"
|
|
13
|
+
super(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/support_table_data.rb
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
module SupportTableData
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
|
+
@data_directory = nil
|
|
12
|
+
|
|
11
13
|
included do
|
|
12
14
|
# Internal variables used for memoization.
|
|
13
15
|
@mutex = Mutex.new
|
|
@@ -17,9 +19,13 @@ module SupportTableData
|
|
|
17
19
|
@support_table_instance_keys = nil
|
|
18
20
|
@support_table_dependencies = []
|
|
19
21
|
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
class_attribute :
|
|
22
|
+
# Private class attribute to hold the key attribute name. Use `support_table_key_attribute` instead.
|
|
23
|
+
# @private
|
|
24
|
+
class_attribute :_support_table_key_attribute, instance_accessor: false
|
|
25
|
+
class << self
|
|
26
|
+
private :_support_table_key_attribute=
|
|
27
|
+
private :_support_table_key_attribute
|
|
28
|
+
end
|
|
23
29
|
|
|
24
30
|
# Define the directory where data files should be loaded from. This value will override the global
|
|
25
31
|
# value set by SupportTableData.data_directory. This is only used if relative paths are passed
|
|
@@ -28,6 +34,20 @@ module SupportTableData
|
|
|
28
34
|
end
|
|
29
35
|
|
|
30
36
|
class_methods do
|
|
37
|
+
# Define the attribute used as the key of the hash in the data files.
|
|
38
|
+
# This should be an attribute with values that never change.
|
|
39
|
+
# By default the key attribute will be the table's primary key.
|
|
40
|
+
def support_table_key_attribute=(attribute_name)
|
|
41
|
+
self._support_table_key_attribute = attribute_name&.to_s
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Get the attribute used as the unique to identify records in the data files.
|
|
45
|
+
#
|
|
46
|
+
# @return [String] The name of the key attribute.
|
|
47
|
+
def support_table_key_attribute
|
|
48
|
+
_support_table_key_attribute || "id"
|
|
49
|
+
end
|
|
50
|
+
|
|
31
51
|
# Synchronize the rows in the table with the values defined in the data files added with
|
|
32
52
|
# `add_support_table_data`. Note that rows will not be deleted if they are no longer in
|
|
33
53
|
# the data files.
|
|
@@ -36,36 +56,41 @@ module SupportTableData
|
|
|
36
56
|
def sync_table_data!
|
|
37
57
|
return unless table_exists?
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
canonical_data = support_table_data.each_with_object({}) do |attributes, hash|
|
|
60
|
+
hash[attributes[support_table_key_attribute].to_s] = attributes
|
|
61
|
+
end
|
|
62
|
+
records = where(support_table_key_attribute => canonical_data.keys)
|
|
42
63
|
changes = []
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
begin
|
|
66
|
+
ActiveSupport::Notifications.instrument("support_table_data.sync", class: self) do
|
|
67
|
+
transaction do
|
|
68
|
+
records.each do |record|
|
|
69
|
+
key = record[support_table_key_attribute].to_s
|
|
70
|
+
attributes = canonical_data.delete(key)
|
|
71
|
+
attributes&.each do |name, value|
|
|
72
|
+
record.send(:"#{name}=", value) if record.respond_to?(:"#{name}=", true)
|
|
73
|
+
end
|
|
74
|
+
if support_table_record_changed?(record)
|
|
75
|
+
changes << record.changes
|
|
76
|
+
record.save!
|
|
77
|
+
end
|
|
51
78
|
end
|
|
52
|
-
|
|
79
|
+
|
|
80
|
+
canonical_data.each_value do |attributes|
|
|
81
|
+
class_name = attributes[inheritance_column]
|
|
82
|
+
klass = class_name ? sti_class_for(class_name) : self
|
|
83
|
+
record = klass.new
|
|
84
|
+
attributes.each do |name, value|
|
|
85
|
+
record.send(:"#{name}=", value) if record.respond_to?(:"#{name}=", true)
|
|
86
|
+
end
|
|
53
87
|
changes << record.changes
|
|
54
88
|
record.save!
|
|
55
89
|
end
|
|
56
90
|
end
|
|
57
|
-
|
|
58
|
-
canonical_data.each_value do |attributes|
|
|
59
|
-
class_name = attributes[inheritance_column]
|
|
60
|
-
klass = class_name ? sti_class_for(class_name) : self
|
|
61
|
-
record = klass.new
|
|
62
|
-
attributes.each do |name, value|
|
|
63
|
-
record.send(:"#{name}=", value) if record.respond_to?(:"#{name}=", true)
|
|
64
|
-
end
|
|
65
|
-
changes << record.changes
|
|
66
|
-
record.save!
|
|
67
|
-
end
|
|
68
91
|
end
|
|
92
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
93
|
+
raise SupportTableData::ValidationError.new(e.record)
|
|
69
94
|
end
|
|
70
95
|
|
|
71
96
|
changes
|
|
@@ -116,14 +141,12 @@ module SupportTableData
|
|
|
116
141
|
# @return [Array<Hash>] List of attributes for all records in the data files.
|
|
117
142
|
def support_table_data
|
|
118
143
|
data = {}
|
|
119
|
-
key_attribute = (support_table_key_attribute || primary_key).to_s
|
|
120
|
-
|
|
121
144
|
@support_table_data_files.each do |data_file_path|
|
|
122
145
|
file_data = support_table_parse_data_file(data_file_path)
|
|
123
146
|
file_data = file_data.values if file_data.is_a?(Hash)
|
|
124
147
|
file_data = Array(file_data).flatten
|
|
125
148
|
file_data.each do |attributes|
|
|
126
|
-
key_value = attributes[
|
|
149
|
+
key_value = attributes[support_table_key_attribute].to_s
|
|
127
150
|
existing = data[key_value]
|
|
128
151
|
if existing
|
|
129
152
|
existing.merge!(attributes)
|
|
@@ -171,9 +194,8 @@ module SupportTableData
|
|
|
171
194
|
# @return [ActiveRecord::Base] The instance loaded from the database.
|
|
172
195
|
# @raise [ActiveRecord::RecordNotFound] If the instance does not exist.
|
|
173
196
|
def named_instance(instance_name)
|
|
174
|
-
key_attribute = (support_table_key_attribute || primary_key).to_s
|
|
175
197
|
instance_name = instance_name.to_s
|
|
176
|
-
find_by!(
|
|
198
|
+
find_by!(support_table_key_attribute => @support_table_instance_names[instance_name])
|
|
177
199
|
end
|
|
178
200
|
|
|
179
201
|
# Get the key values for all instances loaded from the data files.
|
|
@@ -181,13 +203,12 @@ module SupportTableData
|
|
|
181
203
|
# @return [Array] List of all the key attribute values.
|
|
182
204
|
def instance_keys
|
|
183
205
|
if @support_table_instance_keys.nil?
|
|
184
|
-
key_attribute = (support_table_key_attribute || primary_key).to_s
|
|
185
206
|
values = []
|
|
186
207
|
support_table_data.each do |attributes|
|
|
187
|
-
key_value = attributes[
|
|
208
|
+
key_value = attributes[support_table_key_attribute]
|
|
188
209
|
instance = new
|
|
189
|
-
instance.send(:"#{
|
|
190
|
-
values << instance.send(
|
|
210
|
+
instance.send(:"#{support_table_key_attribute}=", key_value)
|
|
211
|
+
values << instance.send(support_table_key_attribute)
|
|
191
212
|
end
|
|
192
213
|
@support_table_instance_keys = values.uniq
|
|
193
214
|
end
|
|
@@ -198,14 +219,12 @@ module SupportTableData
|
|
|
198
219
|
#
|
|
199
220
|
# @return [Boolean]
|
|
200
221
|
def protected_instance?(instance)
|
|
201
|
-
key_attribute = (support_table_key_attribute || primary_key).to_s
|
|
202
|
-
|
|
203
222
|
unless defined?(@protected_keys)
|
|
204
|
-
keys = support_table_data.collect { |attributes| attributes[
|
|
223
|
+
keys = support_table_data.collect { |attributes| attributes[support_table_key_attribute].to_s }
|
|
205
224
|
@protected_keys = keys
|
|
206
225
|
end
|
|
207
226
|
|
|
208
|
-
@protected_keys.include?(instance[
|
|
227
|
+
@protected_keys.include?(instance[support_table_key_attribute].to_s)
|
|
209
228
|
end
|
|
210
229
|
|
|
211
230
|
# Explicitly define other support tables that this model depends on. A support table depends
|
|
@@ -249,12 +268,11 @@ module SupportTableData
|
|
|
249
268
|
raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; name contains illegal characters")
|
|
250
269
|
end
|
|
251
270
|
|
|
252
|
-
|
|
253
|
-
key_value = attributes[key_attribute]
|
|
271
|
+
key_value = attributes[support_table_key_attribute]
|
|
254
272
|
|
|
255
273
|
unless @support_table_instance_names.include?(method_name)
|
|
256
|
-
define_support_table_instance_helper(method_name,
|
|
257
|
-
define_support_table_predicates_helper("#{method_name}?",
|
|
274
|
+
define_support_table_instance_helper(method_name, support_table_key_attribute, key_value)
|
|
275
|
+
define_support_table_predicates_helper("#{method_name}?", support_table_key_attribute, key_value)
|
|
258
276
|
@support_table_instance_names = @support_table_instance_names.merge(method_name => key_value)
|
|
259
277
|
end
|
|
260
278
|
|
|
@@ -342,19 +360,17 @@ module SupportTableData
|
|
|
342
360
|
end
|
|
343
361
|
|
|
344
362
|
class << self
|
|
345
|
-
#
|
|
346
|
-
|
|
363
|
+
# @attribute [r]
|
|
364
|
+
# The the default directory where data files live.
|
|
365
|
+
# @return [String, nil]
|
|
366
|
+
attr_reader :data_directory
|
|
347
367
|
|
|
348
|
-
#
|
|
349
|
-
# then this will be `db/support_tables`. Otherwise, the current working directory will be used.
|
|
368
|
+
# Set the default directory where data files live.
|
|
350
369
|
#
|
|
351
|
-
# @
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
elsif defined?(Rails.root)
|
|
356
|
-
Rails.root.join("db", "support_tables").to_s
|
|
357
|
-
end
|
|
370
|
+
# @param value [String, Pathname, nil] The path to the directory.
|
|
371
|
+
# @return [void]
|
|
372
|
+
def data_directory=(value)
|
|
373
|
+
@data_directory = value&.to_s
|
|
358
374
|
end
|
|
359
375
|
|
|
360
376
|
# Sync all support table classes. Classes must already be loaded in order to be synced.
|
|
@@ -467,6 +483,8 @@ module SupportTableData
|
|
|
467
483
|
end
|
|
468
484
|
end
|
|
469
485
|
|
|
486
|
+
require_relative "support_table_data/validation_error"
|
|
487
|
+
|
|
470
488
|
if defined?(Rails::Railtie)
|
|
471
489
|
require_relative "support_table_data/railtie"
|
|
472
490
|
end
|
|
@@ -3,18 +3,9 @@
|
|
|
3
3
|
namespace :support_table_data do
|
|
4
4
|
desc "Syncronize data for all models that include SupportTableData."
|
|
5
5
|
task sync: :environment do
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if defined?(Rails.application.eager_load!)
|
|
10
|
-
Rails.application.eager_load!
|
|
11
|
-
elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled?
|
|
12
|
-
Rails.autoloaders.each(&:eager_load)
|
|
13
|
-
else
|
|
14
|
-
warn "Could not eager load models; some support table data may not load"
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
|
|
8
|
+
SupportTableData::Tasks::Utils.eager_load!
|
|
18
9
|
|
|
19
10
|
logger_callback = lambda do |name, started, finished, unique_id, payload|
|
|
20
11
|
klass = payload[:class]
|
|
@@ -31,4 +22,56 @@ namespace :support_table_data do
|
|
|
31
22
|
SupportTableData.sync_all!
|
|
32
23
|
end
|
|
33
24
|
end
|
|
25
|
+
|
|
26
|
+
namespace :yard_docs do
|
|
27
|
+
desc "Adds YARD documentation comments to models to document the named instance methods."
|
|
28
|
+
task add: :environment do
|
|
29
|
+
require_relative "../support_table_data/documentation"
|
|
30
|
+
require_relative "utils"
|
|
31
|
+
|
|
32
|
+
SupportTableData::Tasks::Utils.eager_load!
|
|
33
|
+
SupportTableData::Tasks::Utils.support_table_sources.each do |source_file|
|
|
34
|
+
next if source_file.yard_docs_up_to_date?
|
|
35
|
+
|
|
36
|
+
source_file.path.write(source_file.source_with_yard_docs)
|
|
37
|
+
puts "Added YARD documentation to #{source_file.klass.name}."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
desc "Removes YARD documentation comments added by support_table_data from models."
|
|
42
|
+
task remove: :environment do
|
|
43
|
+
require_relative "../support_table_data/documentation"
|
|
44
|
+
require_relative "utils"
|
|
45
|
+
|
|
46
|
+
SupportTableData::Tasks::Utils.eager_load!
|
|
47
|
+
SupportTableData::Tasks::Utils.support_table_sources.each do |source_file|
|
|
48
|
+
next unless source_file.has_yard_docs?
|
|
49
|
+
|
|
50
|
+
source_file.path.write(source_file.source_without_yard_docs)
|
|
51
|
+
puts "Removed YARD documentation from #{source_file.klass.name}."
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc "Verify that all the support table models have up to date YARD documentation for named instance methods."
|
|
56
|
+
task verify: :environment do
|
|
57
|
+
require_relative "../support_table_data/documentation"
|
|
58
|
+
require_relative "utils"
|
|
59
|
+
|
|
60
|
+
SupportTableData::Tasks::Utils.eager_load!
|
|
61
|
+
|
|
62
|
+
all_up_to_date = true
|
|
63
|
+
SupportTableData::Tasks::Utils.support_table_sources.each do |source_file|
|
|
64
|
+
unless source_file.yard_docs_up_to_date?
|
|
65
|
+
puts "YARD documentation is not up to date for #{source_file.klass.name}."
|
|
66
|
+
all_up_to_date = false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if all_up_to_date
|
|
71
|
+
puts "All support table models have up to date YARD documentation."
|
|
72
|
+
else
|
|
73
|
+
raise "Run bundle exec rails support_table_data:yard_docs:add to update the documentation."
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
34
77
|
end
|