support_table_data 1.4.0 → 1.5.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +538 -0
  3. data/CHANGELOG.md +21 -0
  4. data/README.md +52 -5
  5. data/VERSION +1 -1
  6. data/lib/support_table_data/documentation/source_file.rb +98 -0
  7. data/lib/support_table_data/documentation/yard_doc.rb +91 -0
  8. data/lib/support_table_data/documentation.rb +9 -0
  9. data/lib/support_table_data/railtie.rb +22 -1
  10. data/lib/support_table_data/validation_error.rb +16 -0
  11. data/lib/support_table_data.rb +71 -53
  12. data/lib/tasks/support_table_data.rake +61 -12
  13. data/lib/tasks/utils.rb +71 -0
  14. data/support_table_data.gemspec +2 -1
  15. data/test_app/.gitignore +4 -0
  16. data/test_app/Gemfile +7 -0
  17. data/test_app/Rakefile +6 -0
  18. data/test_app/app/models/application_record.rb +5 -0
  19. data/test_app/app/models/secondary_application_record.rb +7 -0
  20. data/test_app/app/models/status.rb +120 -0
  21. data/test_app/app/models/thing.rb +10 -0
  22. data/test_app/bin/rails +4 -0
  23. data/test_app/config/application.rb +42 -0
  24. data/test_app/config/boot.rb +3 -0
  25. data/test_app/config/database.yml +17 -0
  26. data/test_app/config/environment.rb +5 -0
  27. data/test_app/config/environments/development.rb +11 -0
  28. data/test_app/config/environments/test.rb +11 -0
  29. data/test_app/config.ru +6 -0
  30. data/test_app/db/migrate/20260103060951_create_status.rb +8 -0
  31. data/test_app/db/schema.rb +20 -0
  32. data/test_app/db/secondary_migrate/20260104000001_create_things.rb +7 -0
  33. data/test_app/db/secondary_schema.rb +25 -0
  34. data/test_app/db/support_tables/statuses.yml +19 -0
  35. data/test_app/db/support_tables/things.yml +5 -0
  36. data/test_app/lib/tasks/database.rake +11 -0
  37. data/test_app/log/.keep +0 -0
  38. metadata +33 -8
@@ -0,0 +1,98 @@
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
+ UPDATE_COMMAND_COMMENT = "# To update these docs, run `bundle exec rake support_table_data:yard_docs`"
13
+
14
+ # Initialize a new source file representation.
15
+ #
16
+ # @param klass [Class] The model class
17
+ # @param path [Pathname] The path to the source file
18
+ def initialize(klass, path)
19
+ @klass = klass
20
+ @path = path
21
+ @source = nil
22
+ end
23
+
24
+ # Return the source code of the file.
25
+ #
26
+ # @return [String]
27
+ def source
28
+ @source ||= @path.read
29
+ end
30
+
31
+ # Return the source code without any generated YARD documentation.
32
+ #
33
+ # @return [String]
34
+ def source_without_yard_docs
35
+ "#{source.sub(YARD_COMMENT_REGEX, "").rstrip}#{trailing_newline}"
36
+ end
37
+
38
+ # Return the source code with the generated YARD documentation added.
39
+ # The YARD docs are identified by a begin and end comment block. By default
40
+ # the generated docs are added to the end of the file by reopening the class
41
+ # definition. You can move the comment block inside the original class
42
+ # if desired.
43
+ #
44
+ # @return [String]
45
+ def source_with_yard_docs
46
+ yard_docs = YardDoc.new(klass).named_instance_yard_docs
47
+ return source if yard_docs.nil?
48
+
49
+ existing_yard_docs = source.match(YARD_COMMENT_REGEX)
50
+ if existing_yard_docs
51
+ indent = existing_yard_docs[:indent]
52
+ has_class_def = existing_yard_docs.to_s.match?(CLASS_DEF_REGEX)
53
+ yard_docs = yard_docs.lines.map { |line| line.blank? ? "\n" : "#{indent}#{" " if has_class_def}#{line}" }.join
54
+
55
+ updated_source = source[0, existing_yard_docs.begin(0)]
56
+ updated_source << "#{indent}#{BEGIN_YARD_COMMENT}\n"
57
+ updated_source << "#{indent}#{UPDATE_COMMAND_COMMENT}\n"
58
+ updated_source << "#{indent}class #{klass.name}\n" if has_class_def
59
+ updated_source << yard_docs
60
+ updated_source << "\n#{indent}end" if has_class_def
61
+ updated_source << "\n#{indent}#{END_YARD_COMMENT}"
62
+ updated_source << source[existing_yard_docs.end(0)..-1]
63
+ updated_source
64
+ else
65
+ yard_comments = <<~SOURCE.chomp("\n")
66
+ #{BEGIN_YARD_COMMENT}
67
+ #{UPDATE_COMMAND_COMMENT}
68
+ class #{klass.name}
69
+ #{yard_docs.lines.map { |line| line.blank? ? "\n" : " #{line}" }.join}
70
+ end
71
+ #{END_YARD_COMMENT}
72
+ SOURCE
73
+ "#{source.rstrip}\n\n#{yard_comments}#{trailing_newline}"
74
+ end
75
+ end
76
+
77
+ # Check if the YARD documentation in the source file is up to date.
78
+ #
79
+ # @return [Boolean]
80
+ def yard_docs_up_to_date?
81
+ source == source_with_yard_docs
82
+ end
83
+
84
+ # Check if the source file has any YARD documentation added by support_table_data.
85
+ #
86
+ # @return [Boolean]
87
+ def has_yard_docs?
88
+ source.match?(YARD_COMMENT_REGEX)
89
+ end
90
+
91
+ private
92
+
93
+ def trailing_newline
94
+ source.end_with?("\n") ? "\n" : ""
95
+ end
96
+ end
97
+ end
98
+ 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,62 @@ namespace :support_table_data do
31
22
  SupportTableData.sync_all!
32
23
  end
33
24
  end
25
+
26
+ task yard_docs: "yard_docs:add"
27
+
28
+ namespace :yard_docs do
29
+ desc "Adds YARD documentation comments to models to document the named instance methods. Optional arg: file_path"
30
+ task :add, [:file_path] => :environment do |_task, args|
31
+ require_relative "../support_table_data/documentation"
32
+ require_relative "utils"
33
+
34
+ SupportTableData::Tasks::Utils.eager_load!
35
+ SupportTableData::Tasks::Utils.support_table_sources(args[:file_path]).each do |source_file|
36
+ next if source_file.yard_docs_up_to_date?
37
+
38
+ source_file.path.write(source_file.source_with_yard_docs)
39
+ puts "Added YARD documentation to #{source_file.klass.name}."
40
+ end
41
+ end
42
+
43
+ desc "Removes YARD documentation comments added by support_table_data from models. Optional arg: file_path"
44
+ task :remove, [:file_path] => :environment do |_task, args|
45
+ require_relative "../support_table_data/documentation"
46
+ require_relative "utils"
47
+
48
+ SupportTableData::Tasks::Utils.eager_load!
49
+ SupportTableData::Tasks::Utils.support_table_sources(args[:file_path]).each do |source_file|
50
+ next unless source_file.has_yard_docs?
51
+
52
+ source_file.path.write(source_file.source_without_yard_docs)
53
+ puts "Removed YARD documentation from #{source_file.klass.name}."
54
+ end
55
+ end
56
+
57
+ desc "Verify that support table models have up to date YARD docs for named instance methods. Optional arg: file_path"
58
+ task :verify, [:file_path] => :environment do |_task, args|
59
+ require_relative "../support_table_data/documentation"
60
+ require_relative "utils"
61
+
62
+ SupportTableData::Tasks::Utils.eager_load!
63
+
64
+ all_up_to_date = true
65
+ SupportTableData::Tasks::Utils.support_table_sources(args[:file_path]).each do |source_file|
66
+ unless source_file.yard_docs_up_to_date?
67
+ puts "YARD documentation is not up to date for #{source_file.klass.name}."
68
+ all_up_to_date = false
69
+ end
70
+ end
71
+
72
+ if all_up_to_date
73
+ if args[:file_path]
74
+ puts "YARD documentation is up to date for #{args[:file_path]}."
75
+ else
76
+ puts "All support table models have up to date YARD documentation."
77
+ end
78
+ else
79
+ raise "Run bundle exec rake support_table_data:yard_docs to update the documentation."
80
+ end
81
+ end
82
+ end
34
83
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SupportTableData
4
+ module Tasks
5
+ module Utils
6
+ class << self
7
+ # Helper for eager loading a Rails application.
8
+ def eager_load!
9
+ return unless defined?(Rails.application.config.eager_load)
10
+ return if Rails.application.config.eager_load
11
+
12
+ if defined?(Rails.application.eager_load!)
13
+ Rails.application.eager_load!
14
+ elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled?
15
+ Rails.autoloaders.each(&:eager_load)
16
+ else
17
+ raise "Failed to eager load application."
18
+ end
19
+ end
20
+
21
+ # Return all source files for models that include SupportTableData.
22
+ #
23
+ # @param file_path [String, Pathname, nil] Optional file path to filter by.
24
+ # @return [Array<SupportTableData::Documentation::SourceFile>]
25
+ def support_table_sources(file_path = nil)
26
+ require_relative file_path if file_path
27
+
28
+ sources = []
29
+
30
+ ActiveRecord::Base.descendants.each do |klass|
31
+ next unless klass.include?(SupportTableData)
32
+
33
+ begin
34
+ next if klass.instance_names.empty?
35
+ rescue NoMethodError
36
+ # Skip models where instance_names is not properly initialized
37
+ next
38
+ end
39
+
40
+ model_file_path = SupportTableData::Tasks::Utils.model_file_path(klass)
41
+ next unless model_file_path&.file? && model_file_path.readable?
42
+
43
+ sources << Documentation::SourceFile.new(klass, model_file_path)
44
+ end
45
+
46
+ return sources if file_path.nil?
47
+
48
+ resolved_path = Pathname.new(file_path.to_s).expand_path
49
+ sources.select { |source| source.path.expand_path == resolved_path }
50
+ rescue ArgumentError
51
+ []
52
+ end
53
+
54
+ def model_file_path(klass)
55
+ file_path = "#{klass.name.underscore}.rb"
56
+ model_path = nil
57
+
58
+ Rails.application.config.paths["app/models"].each do |path_prefix|
59
+ path = Pathname.new(path_prefix.to_s).join(file_path)
60
+ if path&.file? && path.readable?
61
+ model_path = path
62
+ break
63
+ end
64
+ end
65
+
66
+ model_path
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -12,13 +12,14 @@ Gem::Specification.new do |spec|
12
12
  spec.metadata = {
13
13
  "homepage_uri" => spec.homepage,
14
14
  "source_code_uri" => spec.homepage,
15
- "changelog_uri" => "#{spec.homepage}/blob/master/CHANGELOG.md"
15
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
16
16
  }
17
17
 
18
18
  # Specify which files should be added to the gem when it is released.
19
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
20
  ignore_files = %w[
21
21
  .
22
+ AGENTS.md
22
23
  Appraisals
23
24
  Gemfile
24
25
  Gemfile.lock
@@ -0,0 +1,4 @@
1
+ log/*.log
2
+ tmp/
3
+ doc/
4
+ db/*.sqlite3
data/test_app/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 8.1.1"
4
+
5
+ gem "sqlite3", "~> 2.9.0"
6
+
7
+ gem "support_table_data", path: ".."
data/test_app/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end