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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +140 -0
  3. data/ARCHITECTURE.md +538 -0
  4. data/CHANGELOG.md +13 -0
  5. data/README.md +50 -5
  6. data/VERSION +1 -1
  7. data/lib/support_table_data/documentation/source_file.rb +95 -0
  8. data/lib/support_table_data/documentation/yard_doc.rb +91 -0
  9. data/lib/support_table_data/documentation.rb +9 -0
  10. data/lib/support_table_data/railtie.rb +22 -1
  11. data/lib/support_table_data/validation_error.rb +16 -0
  12. data/lib/support_table_data.rb +71 -53
  13. data/lib/tasks/support_table_data.rake +55 -12
  14. data/lib/tasks/utils.rb +63 -0
  15. data/support_table_data.gemspec +1 -1
  16. data/test_app/.gitignore +4 -0
  17. data/test_app/Gemfile +7 -0
  18. data/test_app/Rakefile +6 -0
  19. data/test_app/app/models/application_record.rb +5 -0
  20. data/test_app/app/models/secondary_application_record.rb +7 -0
  21. data/test_app/app/models/status.rb +11 -0
  22. data/test_app/app/models/thing.rb +10 -0
  23. data/test_app/bin/rails +4 -0
  24. data/test_app/config/application.rb +42 -0
  25. data/test_app/config/boot.rb +3 -0
  26. data/test_app/config/database.yml +17 -0
  27. data/test_app/config/environment.rb +5 -0
  28. data/test_app/config/environments/development.rb +11 -0
  29. data/test_app/config/environments/test.rb +11 -0
  30. data/test_app/config.ru +6 -0
  31. data/test_app/db/migrate/20260103060951_create_status.rb +8 -0
  32. data/test_app/db/schema.rb +20 -0
  33. data/test_app/db/secondary_migrate/20260104000001_create_things.rb +7 -0
  34. data/test_app/db/secondary_schema.rb +25 -0
  35. data/test_app/db/support_tables/statuses.yml +19 -0
  36. data/test_app/db/support_tables/things.yml +5 -0
  37. data/test_app/lib/tasks/database.rake +11 -0
  38. data/test_app/log/.keep +0 -0
  39. 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. In a Rails application, `SupportTableData.data_directory` will be automatically set to `db/support_tables/`. 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.
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 scripts. An easy way to hook it into a Rails application is by enhancing the `db:migrate` task so that the sync task runs immediately after database migrations are run. You can do this by adding code to a Rakefile in your application's `lib/tasks` directory:
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.4.0
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SupportTableData
4
+ module Documentation
5
+ end
6
+ end
7
+
8
+ require_relative "documentation/source_file"
9
+ require_relative "documentation/yard_doc"
@@ -2,8 +2,29 @@
2
2
 
3
3
  module SupportTableData
4
4
  class Railtie < Rails::Railtie
5
- rake_tasks do
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
@@ -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
- # Define the attribute used as the key of the hash in the data files.
21
- # This should be a value that never changes. By default the key attribute will be the id.
22
- class_attribute :support_table_key_attribute, instance_accessor: false
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
- key_attribute = (support_table_key_attribute || primary_key).to_s
40
- canonical_data = support_table_data.each_with_object({}) { |attributes, hash| hash[attributes[key_attribute].to_s] = attributes }
41
- records = where(key_attribute => canonical_data.keys)
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
- ActiveSupport::Notifications.instrument("support_table_data.sync", class: self) do
45
- transaction do
46
- records.each do |record|
47
- key = record[key_attribute].to_s
48
- attributes = canonical_data.delete(key)
49
- attributes&.each do |name, value|
50
- record.send(:"#{name}=", value) if record.respond_to?(:"#{name}=", true)
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
- if support_table_record_changed?(record)
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[key_attribute].to_s
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!(key_attribute => @support_table_instance_names[instance_name])
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[key_attribute]
208
+ key_value = attributes[support_table_key_attribute]
188
209
  instance = new
189
- instance.send(:"#{key_attribute}=", key_value)
190
- values << instance.send(key_attribute)
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[key_attribute].to_s }
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[key_attribute].to_s)
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
- key_attribute = (support_table_key_attribute || primary_key).to_s
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, key_attribute, key_value)
257
- define_support_table_predicates_helper("#{method_name}?", key_attribute, key_value)
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
- # Specify the default directory for data files.
346
- attr_writer :data_directory
363
+ # @attribute [r]
364
+ # The the default directory where data files live.
365
+ # @return [String, nil]
366
+ attr_reader :data_directory
347
367
 
348
- # The directory where data files live by default. If you are running in a Rails environment,
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
- # @return [String]
352
- def data_directory
353
- if defined?(@data_directory)
354
- @data_directory
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
- # Eager load models if we are in a Rails enviroment with eager loading turned off.
7
- if defined?(Rails.application)
8
- unless Rails.application.config.eager_load
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