diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 55b9eab..008301d 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -28,6 +28,8 @@ jobs: - ruby: "ruby" standardrb: true yard: true + - ruby: "4.0" + appraisal: "activerecord_8.1" - ruby: "3.4" appraisal: "activerecord_8.0" - ruby: "3.2" @@ -65,7 +67,7 @@ jobs: run: bundle exec rake - name: standardrb if: matrix.standardrb == true - run: bundle exec rake standard + run: bundle exec standardrb - name: yard if: matrix.yard == true - run: bundle exec yard --fail-on-warning + run: bundle exec yard doc --fail-on-warning diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..028db65 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,140 @@ +# Copilot Instructions for support_table_data + +## Project Overview + +A Ruby gem providing an ActiveRecord mixin for managing support/lookup tables with canonical data defined in YAML/JSON/CSV files. The gem dynamically generates helper methods to reference specific records naturally in code (e.g., `Status.pending` instead of `Status.find_by(name: 'Pending')`). + +**Core concept**: Support tables blur the line between data and code—they contain small canonical datasets that must exist for the application to work. + +## Architecture + +### Key Components + +- **`SupportTableData` module** ([lib/support_table_data.rb](lib/support_table_data.rb)): Main concern mixed into ActiveRecord models +- **Named instance system**: Dynamically generates class methods (`.pending`), predicate methods (`.pending?`), and attribute helpers (`.pending_id`) from hash-based data files +- **Data sync engine**: Compares canonical data files with database records, creating/updating as needed in atomic transactions +- **File parsers**: Supports YAML, JSON, and CSV formats with unified interface + +### Data Flow + +1. Data files (YAML/JSON/CSV) define canonical records with unique key attributes +2. `add_support_table_data` registers file paths and triggers method generation for hash-based files +3. `sync_table_data!` parses files, loads matching DB records, and updates/creates within transactions +4. Named instance methods are dynamically defined via `class_eval` with memoization + +## Development Workflows + +### Running Tests + +```bash +bundle exec rspec # Run all specs +bundle exec rspec spec/support_table_data_spec.rb # Single file +bundle exec rake appraisals # Test against all ActiveRecord versions +``` + +Uses RSpec with in-memory SQLite database. Test models defined in [spec/models.rb](spec/models.rb), data files in `spec/data/`. + +### Testing Against Multiple ActiveRecord Versions + +The gem supports ActiveRecord 6.0-8.0. Uses Appraisal for multi-version testing: + +```bash +bundle exec appraisal install # Install all gemfiles +bundle exec appraisal rspec # Run specs against all versions +``` + +See `Appraisals` file and `gemfiles/` directory. + +### Code Style + +Uses Standard Ruby formatter: + +```bash +bundle exec rake standard:fix # Auto-fix style issues +``` + +## Critical Patterns + +### Named Instance Method Generation + +**Hash-based data files** trigger dynamic method generation. Example from [spec/data/colors/named_colors.yml](spec/data/colors/named_colors.yml): + +```yaml +red: + id: 1 + name: Red + value: 16711680 +``` + +Generates: +- `Color.red` → finds record by id +- `color_instance.red?` → tests if `color_instance.id == 1` +- `Color.red_id` → returns `1` (if `named_instance_attribute_helpers :id` defined) + +**Implementation**: See `define_support_table_named_instance_methods` in [lib/support_table_data.rb](lib/support_table_data.rb#L230-L265). Methods are generated using `class_eval` with string interpolation. + +### Custom Setters for Associations + +Support tables often reference other support tables via named instances. Pattern from [spec/models.rb](spec/models.rb#L72-L74): + +```ruby +def group_name=(value) + self.group = Group.named_instance(value) +end +``` + +Allows data files to reference related records by instance name instead of foreign keys. + +### Key Attribute Configuration + +By default, uses model's `primary_key`. Override for non-id keys: + +```ruby +self.support_table_key_attribute = :name # Use 'name' instead of 'id' +``` + +Key attributes cannot be updated—changing them creates new records. + +### Dependency Resolution + +`sync_all!` automatically resolves dependencies via `belongs_to` associations and loads tables in correct order. For complex cases (join tables, indirect dependencies), explicitly declare: + +```ruby +support_table_dependency "OtherModel" +``` + +See [lib/support_table_data.rb](lib/support_table_data.rb#L219-L222) and dependency resolution logic. + +## Testing Conventions + +- **Test data isolation**: Each test deletes all records in `before` block ([spec/spec_helper.rb](spec/spec_helper.rb)) +- **Sync before assertions**: Tests call `sync_table_data!` or `sync_all!` before verifying records exist +- **Multi-file merging**: Tests verify that multiple data files for same model merge correctly (see `Color` model with 5 data files) +- **STI handling**: See `Polygon`/`Triangle`/`Rectangle` tests for Single Table Inheritance patterns + +## Common Pitfalls + +1. **Method name conflicts**: Named instance methods raise `ArgumentError` if method already exists. Instance names must match `/\A[a-z][a-z0-9_]+\z/` +2. **Array vs hash data**: Only hash-keyed data generates named instance methods. Use arrays or underscore-prefixed keys (`_others`) for records without helpers +3. **Protected instances**: Records in data files cannot be deleted via `destroy` (though this gem doesn't enforce it—see companion caching gem) +4. **Transaction safety**: All sync operations wrapped in transactions; changes rollback on failure + +## Rails Integration + +In Rails apps, the gem automatically: +- Sets `SupportTableData.data_directory` to `Rails.root/db/support_tables` +- Provides `rake support_table_data:sync` task ([lib/tasks/support_table_data.rake](lib/tasks/support_table_data.rake)) +- Handles eager loading in both classic and Zeitwerk autoloaders + +## File References + +- Main module: [lib/support_table_data.rb](lib/support_table_data.rb) +- Test models: [spec/models.rb](spec/models.rb) - comprehensive examples of patterns +- Sync task: [lib/tasks/support_table_data.rake](lib/tasks/support_table_data.rake) +- Architecture docs: [ARCHITECTURE.md](ARCHITECTURE.md) - detailed diagrams and design decisions + +## Version Compatibility + +- Ruby ≥ 2.5 +- ActiveRecord ≥ 6.0 +- Ruby 3.4+: Requires `csv` gem in Gemfile (removed from stdlib) diff --git a/Appraisals b/Appraisals index 52818d3..379eaaf 100644 --- a/Appraisals +++ b/Appraisals @@ -1,5 +1,10 @@ # frozen_string_literal: true +appraise "activerecord_8.1" do + gem "activerecord", "~> 8.1.0" + gem "sqlite3", "~> 2.9.0" +end + appraise "activerecord_8.0" do gem "activerecord", "~> 8.0.0" gem "sqlite3", "~> 2.5.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index cd2e099..5efdde9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.4.1 + +### Added + +- The default data directory in a Rails application can be set with the `config.support_table_data_directory` option in the Rails application configuration. +- Added rake task `support_table_data:add_yard_docs` for Rails applications that will add YARD documentation to support table models for the named instance helpers. + +### Changed + +- The default data directory is now set in a Railtie and can be overridden with the `config.support_table_data_directory` option in the Rails application configuration. + ## 1.4.0 ### Fixed diff --git a/README.md b/README.md index 8db62e3..e2cb683 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ class Status < ApplicationRecord 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. -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. +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. + +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. **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. @@ -109,7 +111,6 @@ Helper methods will not override already defined methods on a model class. If a 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. - ```ruby class Status < ApplicationRecord include SupportTableData @@ -171,6 +172,19 @@ completed: group_name: done ``` +#### Documenting Named Instance Helpers + +In a Rails application, you can add YARD documentation for the named instance helpers by running the rake task `support_table_data:add_yard_docs`. 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. + +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. + +```ruby +# Begin YARD docs for support_table_data +# End YARD docs for support_table_data +``` + +A good practice is to add a check to your CI pipeline to ensure the documentation is always up to date. + ### Caching 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. diff --git a/Rakefile b/Rakefile index 584050e..3deea70 100644 --- a/Rakefile +++ b/Rakefile @@ -4,9 +4,6 @@ rescue LoadError puts "You must `gem install bundler` and `bundle install` to run rake tasks" end -require "yard" -YARD::Rake::YardocTask.new(:yard) - require "bundler/gem_tasks" task :verify_release_branch do @@ -23,17 +20,3 @@ require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) task default: :spec - -desc "run the specs using appraisal" -task :appraisals do - exec "bundle exec appraisal rake spec" -end - -namespace :appraisals do - desc "install all the appraisal gemspecs" - task :install do - exec "bundle exec appraisal install" - end -end - -require "standard/rake" diff --git a/VERSION b/VERSION index 88c5fb8..347f583 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.1 diff --git a/gemfiles/activerecord_8.1.gemfile b/gemfiles/activerecord_8.1.gemfile new file mode 100644 index 0000000..0e7ee30 --- /dev/null +++ b/gemfiles/activerecord_8.1.gemfile @@ -0,0 +1,15 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rspec", "~> 3.0" +gem "rake" +gem "sqlite3", "~> 2.9.0" +gem "appraisal" +gem "standard", "~>1.0" +gem "pry-byebug" +gem "yard" +gem "csv" +gem "activerecord", "~> 8.1.0" + +gemspec path: "../" diff --git a/lib/support_table_data.rb b/lib/support_table_data.rb index f002120..12bb920 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -8,6 +8,8 @@ module SupportTableData extend ActiveSupport::Concern + @data_directory = nil + included do # Internal variables used for memoization. @mutex = Mutex.new @@ -342,19 +344,17 @@ def support_table_record_changed?(record, seen = Set.new) end class << self - # Specify the default directory for data files. - attr_writer :data_directory + # @attribute [r] + # The the default directory where data files live. + # @return [String, nil] + attr_reader :data_directory - # The directory where data files live by default. If you are running in a Rails environment, - # then this will be `db/support_tables`. Otherwise, the current working directory will be used. + # Set the default directory where data files live. # - # @return [String] - def data_directory - if defined?(@data_directory) - @data_directory - elsif defined?(Rails.root) - Rails.root.join("db", "support_tables").to_s - end + # @param value [String, Pathname, nil] The path to the directory. + # @return [void] + def data_directory=(value) + @data_directory = value&.to_s end # Sync all support table classes. Classes must already be loaded in order to be synced. diff --git a/lib/support_table_data/documentation.rb b/lib/support_table_data/documentation.rb new file mode 100644 index 0000000..248baf5 --- /dev/null +++ b/lib/support_table_data/documentation.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module SupportTableData + module Documentation + end +end + +require_relative "documentation/source_file" +require_relative "documentation/yard_doc" diff --git a/lib/support_table_data/documentation/source_file.rb b/lib/support_table_data/documentation/source_file.rb new file mode 100644 index 0000000..186b376 --- /dev/null +++ b/lib/support_table_data/documentation/source_file.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module SupportTableData + module Documentation + class SourceFile + attr_reader :klass, :path + + BEGIN_YARD_COMMENT = "# Begin YARD docs for support_table_data" + END_YARD_COMMENT = "# End YARD docs for support_table_data" + YARD_COMMENT_REGEX = /^(?[ \t]*)#{BEGIN_YARD_COMMENT}.*^[ \t]*#{END_YARD_COMMENT}$/m + CLASS_DEF_REGEX = /^[ \t]*class [a-zA-Z_0-9:]+.*?$/ + + # Initialize a new source file representation. + # + # @param klass [Class] The model class + # @param path [Pathname] The path to the source file + def initialize(klass, path) + @klass = klass + @path = path + @source = nil + end + + # Return the source code of the file. + # + # @return [String] + def source + @source ||= @path.read + end + + # Return the source code without any generated YARD documentation. + # + # @return [String] + def source_without_yard_docs + "#{source.sub(YARD_COMMENT_REGEX, "").rstrip}#{trailing_newline}" + end + + # Return the source code with the generated YARD documentation added. + # The YARD docs are identified by a begin and end comment block. By default + # the generated docs are added to the end of the file by reopening the class + # definition. You can move the comment block inside the original class + # if desired. + # + # @return [String] + def source_with_yard_docs + yard_docs = YardDoc.new(klass).named_instance_yard_docs + return source if yard_docs.nil? + + existing_yard_docs = source.match(YARD_COMMENT_REGEX) + if existing_yard_docs + indent = existing_yard_docs[:indent] + has_class_def = existing_yard_docs.to_s.match?(CLASS_DEF_REGEX) + yard_docs = yard_docs.lines.map { |line| line.blank? ? "\n" : "#{indent}#{" " if has_class_def}#{line}" }.join + + updated_source = source[0, existing_yard_docs.begin(0)] + updated_source << "#{indent}#{BEGIN_YARD_COMMENT}\n" + updated_source << "#{indent}class #{klass.name}\n" if has_class_def + updated_source << yard_docs + updated_source << "\n#{indent}end" if has_class_def + updated_source << "\n#{indent}#{END_YARD_COMMENT}" + updated_source << source[existing_yard_docs.end(0)..-1] + updated_source + else + yard_comments = <<~SOURCE.chomp("\n") + #{BEGIN_YARD_COMMENT} + class #{klass.name} + #{yard_docs.lines.map { |line| line.blank? ? "\n" : " #{line}" }.join} + end + #{END_YARD_COMMENT} + SOURCE + "#{source.rstrip}\n\n#{yard_comments}#{trailing_newline}" + end + end + + # Check if the YARD documentation in the source file is up to date. + # + # @return [Boolean] + def yard_docs_up_to_date? + source == source_with_yard_docs + end + + private + + def trailing_newline + source.end_with?("\n") ? "\n" : "" + end + end + end +end diff --git a/lib/support_table_data/documentation/yard_doc.rb b/lib/support_table_data/documentation/yard_doc.rb new file mode 100644 index 0000000..2fa6baa --- /dev/null +++ b/lib/support_table_data/documentation/yard_doc.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module SupportTableData + module Documentation + class YardDoc + # @param klass [Class] The model class to generate documentation for + def initialize(klass) + @klass = klass + end + + # Generate YARD documentation class definition for the model's helper methods. + # + # @return [String, nil] The YARD documentation class definition, or nil if no named instances + def named_instance_yard_docs + instance_names = klass.instance_names + generate_yard_docs(instance_names) + end + + # Generate YARD documentation comment for named instance singleton method. + # + # @param name [String] The name of the instance method. + # @return [String] The YARD comment text + def instance_helper_yard_doc(name) + <<~YARD.chomp("\n") + # Find the named instance +#{name}+ from the database. + # + # @!method self.#{name} + # @return [#{klass.name}] + # @raise [ActiveRecord::RecordNotFound] if the record does not exist + # @!visibility public + YARD + end + + # Generate YARD documentation comment for the predicate method for the named instance. + # + # @param name [String] The name of the instance method. + # @return [String] The YARD comment text + def predicate_helper_yard_doc(name) + <<~YARD.chomp("\n") + # Check if this record is the named instance +#{name}+. + # + # @!method #{name}? + # @return [Boolean] + # @!visibility public + YARD + end + + # Generate YARD documentation comment for the attribute method helper for the named instance. + # + # @param name [String] The name of the instance method. + # @return [String] The YARD comment text + def attribute_helper_yard_doc(name, attribute_name) + <<~YARD.chomp("\n") + # Get the #{attribute_name} attribute from the data file + # for the named instance +#{name}+. + # + # @!method self.#{name}_#{attribute_name} + # @return [Object] + # @!visibility public + YARD + end + + private + + attr_reader :klass + + def generate_yard_docs(instance_names) + return nil if instance_names.empty? + + yard_lines = ["# @!group Named Instances"] + + # Generate docs for each named instance + instance_names.sort.each do |name| + yard_lines << "" + yard_lines << instance_helper_yard_doc(name) + yard_lines << "" + yard_lines << predicate_helper_yard_doc(name) + klass.support_table_attribute_helpers.each do |attribute_name| + yard_lines << "" + yard_lines << attribute_helper_yard_doc(name, attribute_name) + end + end + + yard_lines << "" + yard_lines << "# @!endgroup" + + yard_lines.join("\n") + end + end + end +end diff --git a/lib/support_table_data/railtie.rb b/lib/support_table_data/railtie.rb index fd31e1e..ea84142 100644 --- a/lib/support_table_data/railtie.rb +++ b/lib/support_table_data/railtie.rb @@ -2,6 +2,12 @@ module SupportTableData class Railtie < Rails::Railtie + config.support_table_data_directory = "db/support_tables" + + initializer "support_table_data" do |app| + SupportTableData.data_directory ||= app.root.join(app.config.support_table_data_directory).to_s + end + rake_tasks do load File.expand_path("../tasks/support_table_data.rake", __dir__) end diff --git a/lib/tasks/support_table_data.rake b/lib/tasks/support_table_data.rake index f21ed44..8b53cc6 100644 --- a/lib/tasks/support_table_data.rake +++ b/lib/tasks/support_table_data.rake @@ -3,18 +3,9 @@ namespace :support_table_data do desc "Syncronize data for all models that include SupportTableData." task sync: :environment do - # Eager load models if we are in a Rails enviroment with eager loading turned off. - if defined?(Rails.application) - unless Rails.application.config.eager_load - if defined?(Rails.application.eager_load!) - Rails.application.eager_load! - elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled? - Rails.autoloaders.each(&:eager_load) - else - warn "Could not eager load models; some support table data may not load" - end - end - end + require_relative "utils" + + SupportTableData::Tasks::Utils.eager_load! logger_callback = lambda do |name, started, finished, unique_id, payload| klass = payload[:class] @@ -31,4 +22,18 @@ namespace :support_table_data do SupportTableData.sync_all! end end + + desc "Adds YARD documentation comments to models to document the named instance methods." + task add_yard_docs: :environment do + require_relative "../support_table_data/documentation" + require_relative "utils" + + SupportTableData::Tasks::Utils.eager_load! + SupportTableData::Tasks::Utils.support_table_sources.each do |source_file| + next if source_file.yard_docs_up_to_date? + + source_file.path.write(source_file.source_with_yard_docs) + puts "Added YARD documentation to #{source_file.klass.name}." + end + end end diff --git a/lib/tasks/utils.rb b/lib/tasks/utils.rb new file mode 100644 index 0000000..108fb2a --- /dev/null +++ b/lib/tasks/utils.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module SupportTableData + module Tasks + module Utils + class << self + # Helper for eager loading a Rails application. + def eager_load! + return unless defined?(Rails.application.config.eager_load) + return if Rails.application.config.eager_load + + if defined?(Rails.application.eager_load!) + Rails.application.eager_load! + elsif defined?(Rails.autoloaders.zeitwerk_enabled?) && Rails.autoloaders.zeitwerk_enabled? + Rails.autoloaders.each(&:eager_load) + else + raise "Failed to eager load application." + end + end + + # Return a hash mapping all models that include SupportTableData to their source file paths. + # + # @return [Array] + def support_table_sources + sources = [] + + ActiveRecord::Base.descendants.each do |klass| + next unless klass.included_modules.include?(SupportTableData) + + begin + next if klass.instance_names.empty? + rescue NoMethodError + # Skip models where instance_names is not properly initialized + next + end + + file_path = SupportTableData::Tasks::Utils.model_file_path(klass) + next unless file_path&.file? && file_path.readable? + + sources << Documentation::SourceFile.new(klass, file_path) + end + + sources + end + + def model_file_path(klass) + file_path = "#{klass.name.underscore}.rb" + model_path = nil + + Rails.application.config.paths["app/models"].each do |path_prefix| + path = Pathname.new(path_prefix.to_s).join(file_path) + if path&.file? && path.readable? + model_path = path + break + end + end + + model_path + end + end + end + end +end diff --git a/spec/models.rb b/spec/models.rb index 6fed774..706eb2b 100644 --- a/spec/models.rb +++ b/spec/models.rb @@ -48,138 +48,15 @@ end end -class Color < ActiveRecord::Base - include SupportTableData - - self.support_table_data_directory = File.join(__dir__, "data", "colors") - add_support_table_data "named_colors.yml" - add_support_table_data "named_colors.json" - add_support_table_data "colors.yml" - add_support_table_data File.join(__dir__, "data", "colors", "colors.json") - add_support_table_data "colors.csv" - - belongs_to :group - belongs_to :hue - has_many :things - has_many :shades, through: :things - has_many :aliases, autosave: true - - # Intentionally invalid association - belongs_to :non_existent, class_name: "NonExistent" - - validates_uniqueness_of :name - - def group_name=(value) - self.group = Group.named_instance(value) - end - - def hue_name=(value) - self.hue = Hue.find_by!(name: value) - end - - def alias_names=(names) - self.aliases = names.map { |name| Alias.find_or_initialize_by(name: name) } - end - - private - - def hex=(value) - self.value = value.to_i(16) - end -end - -class Alias < ActiveRecord::Base - belongs_to :color - - validates_uniqueness_of :name -end - -class Group < ActiveRecord::Base - include SupportTableData - - self.primary_key = :group_id - - named_instance_attribute_helpers :group_id - - add_support_table_data "groups.yml" - - named_instance_attribute_helpers :name - - validates_uniqueness_of :name -end - -class Hue < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - add_support_table_data "hues.yml" - - belongs_to :parent, class_name: "Hue", optional: true - - validates_uniqueness_of :name - - def parent_name=(value) - self.parent = Hue.find_by!(name: value) - end - - has_many :shade_hues - has_many :shades, through: :shade_hues, autosave: true - - support_table_dependency "Shade" - - def shade_names=(names) - self.shades = Shade.where(name: names) - end -end - -class Shade < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - add_support_table_data "shades.yml" - - validates_uniqueness_of :name - - has_many :shade_hues - has_many :hues, through: :shade_hues -end - -class ShadeHue < ActiveRecord::Base - belongs_to :shade - belongs_to :hue -end - -class Thing < ActiveRecord::Base - belongs_to :color - belongs_to :shade -end - -class Invalid < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - def already_defined? - true - end -end - -class Polygon < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - add_support_table_data "polygons.yml" - - validates :name, uniqueness: true -end - -class Triangle < Polygon - validates :side_count, numericality: {equal_to: 3} -end - -class Rectangle < Polygon - validates :side_count, numericality: {equal_to: 4} -end +# Lazy load model classes +autoload :Alias, File.expand_path("models/alias.rb", __dir__) +autoload :Color, File.expand_path("models/color.rb", __dir__) +autoload :Group, File.expand_path("models/group.rb", __dir__) +autoload :Hue, File.expand_path("models/hue.rb", __dir__) +autoload :Invalid, File.expand_path("models/invalid.rb", __dir__) +autoload :Polygon, File.expand_path("models/polygon.rb", __dir__) +autoload :Rectangle, File.expand_path("models/rectangle.rb", __dir__) +autoload :Shade, File.expand_path("models/shade.rb", __dir__) +autoload :ShadeHue, File.expand_path("models/shade_hue.rb", __dir__) +autoload :Thing, File.expand_path("models/thing.rb", __dir__) +autoload :Triangle, File.expand_path("models/triangle.rb", __dir__) diff --git a/spec/models/alias.rb b/spec/models/alias.rb new file mode 100644 index 0000000..31789ac --- /dev/null +++ b/spec/models/alias.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Alias < ActiveRecord::Base + belongs_to :color + + validates_uniqueness_of :name +end diff --git a/spec/models/color.rb b/spec/models/color.rb new file mode 100644 index 0000000..d429ca7 --- /dev/null +++ b/spec/models/color.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class Color < ActiveRecord::Base + include SupportTableData + + self.support_table_data_directory = File.join(__dir__, "..", "data", "colors") + add_support_table_data "named_colors.yml" + add_support_table_data "named_colors.json" + add_support_table_data "colors.yml" + add_support_table_data File.join(__dir__, "..", "data", "colors", "colors.json") + add_support_table_data "colors.csv" + + belongs_to :group + belongs_to :hue + has_many :things + has_many :shades, through: :things + has_many :aliases, autosave: true + + # Intentionally invalid association + belongs_to :non_existent, class_name: "NonExistent" + + validates_uniqueness_of :name + + def group_name=(value) + self.group = Group.named_instance(value) + end + + def hue_name=(value) + self.hue = Hue.find_by!(name: value) + end + + def alias_names=(names) + self.aliases = names.map { |name| Alias.find_or_initialize_by(name: name) } + end + + private + + def hex=(value) + self.value = value.to_i(16) + end +end diff --git a/spec/models/group.rb b/spec/models/group.rb new file mode 100644 index 0000000..4f0dd0c --- /dev/null +++ b/spec/models/group.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Group < ActiveRecord::Base + include SupportTableData + + self.primary_key = :group_id + + named_instance_attribute_helpers :group_id + + add_support_table_data "groups.yml" + + named_instance_attribute_helpers :name + + validates_uniqueness_of :name +end diff --git a/spec/models/hue.rb b/spec/models/hue.rb new file mode 100644 index 0000000..120d8f1 --- /dev/null +++ b/spec/models/hue.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Hue < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "hues.yml" + + belongs_to :parent, class_name: "Hue", optional: true + + validates_uniqueness_of :name + + def parent_name=(value) + self.parent = Hue.find_by!(name: value) + end + + has_many :shade_hues + has_many :shades, through: :shade_hues, autosave: true + + support_table_dependency "Shade" + + def shade_names=(names) + self.shades = Shade.where(name: names) + end +end diff --git a/spec/models/invalid.rb b/spec/models/invalid.rb new file mode 100644 index 0000000..733e5ca --- /dev/null +++ b/spec/models/invalid.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Invalid < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + def already_defined? + true + end +end diff --git a/spec/models/polygon.rb b/spec/models/polygon.rb new file mode 100644 index 0000000..10359cf --- /dev/null +++ b/spec/models/polygon.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Polygon < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "polygons.yml" + + validates :name, uniqueness: true +end diff --git a/spec/models/rectangle.rb b/spec/models/rectangle.rb new file mode 100644 index 0000000..f0aaab9 --- /dev/null +++ b/spec/models/rectangle.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Rectangle < Polygon + validates :side_count, numericality: {equal_to: 4} +end diff --git a/spec/models/shade.rb b/spec/models/shade.rb new file mode 100644 index 0000000..d1d3011 --- /dev/null +++ b/spec/models/shade.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Shade < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "shades.yml" + + validates_uniqueness_of :name + + has_many :shade_hues + has_many :hues, through: :shade_hues +end diff --git a/spec/models/shade_hue.rb b/spec/models/shade_hue.rb new file mode 100644 index 0000000..a2dd88c --- /dev/null +++ b/spec/models/shade_hue.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ShadeHue < ActiveRecord::Base + belongs_to :shade + belongs_to :hue +end diff --git a/spec/models/thing.rb b/spec/models/thing.rb new file mode 100644 index 0000000..2326365 --- /dev/null +++ b/spec/models/thing.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Thing < ActiveRecord::Base + belongs_to :color + belongs_to :shade +end diff --git a/spec/models/triangle.rb b/spec/models/triangle.rb new file mode 100644 index 0000000..d75fc0a --- /dev/null +++ b/spec/models/triangle.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Triangle < Polygon + validates :side_count, numericality: {equal_to: 3} +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fe9b1cd..9ff17b7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => ":memory:") require_relative "../lib/support_table_data" +require_relative "../lib/support_table_data/documentation" SupportTableData.data_directory = File.join(__dir__, "data") diff --git a/spec/support_table_data/documentation/source_file_spec.rb b/spec/support_table_data/documentation/source_file_spec.rb new file mode 100644 index 0000000..075c165 --- /dev/null +++ b/spec/support_table_data/documentation/source_file_spec.rb @@ -0,0 +1,232 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SupportTableData::Documentation::SourceFile do + let(:color_path) { Pathname.new(File.expand_path("../../models/color.rb", __dir__)) } + let(:group_path) { Pathname.new(File.expand_path("../../models/group.rb", __dir__)) } + + describe "#source" do + it "reads and caches the file content" do + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + source = source_file.source + + expect(source).to be_a(String) + expect(source).to include("class Color < ActiveRecord::Base") + + # Verify caching - should return same object + expect(source_file.source).to equal(source) + end + end + + describe "#source_without_yard_docs" do + it "removes existing YARD documentation between markers" do + source_with_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + + # Begin YARD docs for support_table_data + class Color + # Some YARD docs + end + # End YARD docs for support_table_data + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_docs) + + result = source_file.source_without_yard_docs + + expect(result).to include("class Color < ActiveRecord::Base") + expect(result).to include("include SupportTableData") + expect(result).not_to include("Begin YARD docs") + expect(result).not_to include("Some YARD docs") + expect(result).not_to include("End YARD docs") + end + + it "preserves trailing newline if present in original" do + source_with_newline = "class Color\nend\n" + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_newline) + + result = source_file.source_without_yard_docs + + expect(result).to end_with("\n") + end + + it "preserves no trailing newline if absent in original" do + source_without_newline = "class Color\nend" + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_without_newline) + + result = source_file.source_without_yard_docs + + expect(result).not_to end_with("\n") + end + + it "returns original source if no YARD docs present" do + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + original = source_file.source + result = source_file.source_without_yard_docs + + # Both should have same content (though not necessarily same object) + expect(result.strip).to eq(original.strip) + end + end + + describe "#source_with_yard_docs" do + it "adds YARD documentation with proper markers" do + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + result = source_file.source_with_yard_docs + + expect(result).to include("# Begin YARD docs for support_table_data") + expect(result).to include("# End YARD docs for support_table_data") + + # Color has named instances: black, blue, green, red + expect(result).to include("# @!method self.black") + expect(result).to include("# @!method black?") + end + + it "preserves trailing newline from original file" do + source_with_newline = "class Color\nend\n" + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_newline) + + result = source_file.source_with_yard_docs + + expect(result).to end_with("\n") + end + + it "preserves no trailing newline if absent in original" do + source_without_newline = "class Color\nend" + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_without_newline) + + result = source_file.source_with_yard_docs + + # When appending new docs, they don't add trailing newline if original doesn't have one + expect(result).not_to end_with("\n") + end + + it "replaces existing YARD docs with fresh ones when markers include class definition" do + source_with_old_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + + # Begin YARD docs for support_table_data + class Color + # Old YARD docs + end + # End YARD docs for support_table_data + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_old_docs) + + result = source_file.source_with_yard_docs + + expect(result).to include("# Begin YARD docs for support_table_data") + expect(result).to include("# End YARD docs for support_table_data") + expect(result).not_to include("# Old YARD docs") + expect(result).to include("# @!method self.black") + # Should include class definition since original had it + expect(result).to match(/# Begin YARD docs.*class Color.*# @!method self\.black.*end.*# End YARD docs/m) + end + + it "replaces existing YARD docs inline when markers do not include class definition" do + source_with_inline_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + + # Begin YARD docs for support_table_data + # Old inline YARD docs + # End YARD docs for support_table_data + + def some_method + end + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_inline_docs) + + result = source_file.source_with_yard_docs + + expect(result).to include("# Begin YARD docs for support_table_data") + expect(result).to include("# End YARD docs for support_table_data") + expect(result).not_to include("# Old inline YARD docs") + expect(result).to include("# @!method self.black") + # Should NOT reopen class definition since original didn't have it + expect(result).not_to match(/# Begin YARD docs.*class Color.*# End YARD docs/m) + # Verify the class definition remains at the top and inline docs are between markers + expect(result).to match(/class Color < ActiveRecord::Base.*# Begin YARD docs.*# @!method self\.black.*# End YARD docs.*def some_method/m) + end + + it "preserves indentation when replacing inline YARD docs" do + source_with_indented_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + + # Begin YARD docs for support_table_data + # Old docs + # End YARD docs for support_table_data + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_indented_docs) + + result = source_file.source_with_yard_docs + + # Check that the generated docs maintain the 2-space indentation + expect(result).to include(" # Begin YARD docs for support_table_data") + expect(result).to include(" # @!method self.black") + expect(result).to include(" # End YARD docs for support_table_data") + end + end + + describe "#yard_docs_up_to_date?" do + it "returns true when YARD docs match current generated docs" do + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + up_to_date_source = source_file.source_with_yard_docs + + allow(source_file).to receive(:source).and_return(up_to_date_source) + + expect(source_file.yard_docs_up_to_date?).to be true + end + + it "returns false when YARD docs are missing" do + source_without_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_without_docs) + + expect(source_file.yard_docs_up_to_date?).to be false + end + + it "returns false when YARD docs are outdated" do + source_with_old_docs = <<~RUBY + class Color < ActiveRecord::Base + include SupportTableData + + # Begin YARD docs for support_table_data + class Color + # Old outdated docs + end + # End YARD docs for support_table_data + end + RUBY + + source_file = SupportTableData::Documentation::SourceFile.new(Color, color_path) + allow(source_file).to receive(:source).and_return(source_with_old_docs) + + expect(source_file.yard_docs_up_to_date?).to be false + end + end +end diff --git a/spec/support_table_data/documentation/yard_doc_spec.rb b/spec/support_table_data/documentation/yard_doc_spec.rb new file mode 100644 index 0000000..b9fb623 --- /dev/null +++ b/spec/support_table_data/documentation/yard_doc_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SupportTableData::Documentation::YardDoc do + describe "#instance_helper_yard_doc" do + it "generates YARD documentation for a named instance class method" do + doc = SupportTableData::Documentation::YardDoc.new(Color) + result = doc.instance_helper_yard_doc("red") + + expect(result).to include("# Find the named instance +red+ from the database.") + expect(result).to include("# @return [Color]") + expect(result).to include("# @raise [ActiveRecord::RecordNotFound] if the record does not exist") + expect(result).to include("# @!method self.red") + end + + it "uses the correct class name in return type" do + doc = SupportTableData::Documentation::YardDoc.new(Group) + result = doc.instance_helper_yard_doc("primary") + + expect(result).to include("# @return [Group]") + end + end + + describe "#predicate_helper_yard_doc" do + it "generates YARD documentation for a named instance predicate method" do + doc = SupportTableData::Documentation::YardDoc.new(Color) + result = doc.predicate_helper_yard_doc("red") + + expect(result).to include("# Check if this record is the named instance +red+.") + expect(result).to include("# @return [Boolean]") + expect(result).to include("# @!method red?") + end + end + + describe "#named_instance_yard_docs" do + it "returns nil when model has no named instances" do + allow(Color).to receive(:instance_names).and_return([]) + + doc = SupportTableData::Documentation::YardDoc.new(Color) + result = doc.named_instance_yard_docs + + expect(result).to be_nil + end + + it "generates YARD docs for all named instances" do + doc = SupportTableData::Documentation::YardDoc.new(Color) + result = doc.named_instance_yard_docs + + expect(result).not_to be_nil + + # Check for group markers + expect(result).to include("# @!group Named Instances") + expect(result).to include("# @!endgroup") + + # Check for named instances (Color has black, blue, green, red) + expect(result).to include("# Find the named instance +black+ from the database.") + expect(result).to include("# @!method self.black") + expect(result).to include("# Check if this record is the named instance +black+.") + expect(result).to include("# @!method black?") + + expect(result).to include("# Find the named instance +blue+ from the database.") + expect(result).to include("# @!method self.blue") + end + + it "includes attribute helper methods when defined" do + doc = SupportTableData::Documentation::YardDoc.new(Group) + result = doc.named_instance_yard_docs + + expect(result).not_to be_nil + + # Group has attribute helpers for group_id and name + # Check for one of Group's instances (e.g., gray, primary, secondary) + expect(result).to include("# Get the group_id attribute from the data file") + expect(result).to include("# for the named instance +gray+.") + expect(result).to include("# @!method self.gray_group_id") + expect(result).to include("# Get the name attribute from the data file") + expect(result).to include("# for the named instance +gray+.") + expect(result).to include("# @!method self.gray_name") + end + + it "sorts instance names alphabetically" do + doc = SupportTableData::Documentation::YardDoc.new(Color) + result = doc.named_instance_yard_docs + + # Color instances should appear in alphabetical order + # Use more specific patterns to find the first occurrence of each method + black_pos = result.index("# @!method self.black") + blue_pos = result.index("# @!method self.blue") + green_pos = result.index("# @!method self.green") + red_pos = result.index("# @!method self.red") + + expect(black_pos).to be < blue_pos + expect(blue_pos).to be < green_pos + expect(green_pos).to be < red_pos + end + end +end diff --git a/spec/tasks_spec.rb b/spec/tasks_spec.rb index d2bdf3f..48cf4e9 100644 --- a/spec/tasks_spec.rb +++ b/spec/tasks_spec.rb @@ -3,15 +3,23 @@ require_relative "spec_helper" require "rake" -describe "support_table_data" do - let(:rake) { Rake::Application.new } +RSpec.describe "support_table_data" do let(:out) { StringIO.new } before do - Rake.application = rake - Rake.application.rake_require("lib/tasks/support_table_data", [File.join(__dir__, "..")]) + # Create a fresh Rake application for each test + Rake.application = Rake::Application.new + load File.join(__dir__, "..", "lib", "tasks", "support_table_data.rake") Rake::Task.define_task(:environment) allow(ActiveRecord::Base).to receive(:logger).and_return(Logger.new(out)) + + # Mock Rails for task execution + rails_app = double("Rails.application") + rails_config = double("Rails.application.config") + allow(rails_config).to receive(:eager_load).and_return(true) # Already loaded in spec_helper + allow(rails_config).to receive(:paths).and_return("app/models" => [File.join(__dir__, "models")]) + allow(rails_app).to receive(:config).and_return(rails_config) + stub_const("Rails", double("Rails", application: rails_app)) end describe "sync" do @@ -23,4 +31,33 @@ end end end + + describe "add_yard_docs" do + it "adds YARD documentation to models with named instances" do + require_relative "../lib/support_table_data/documentation" + require_relative "../lib/tasks/utils" + + # Mock stdout to capture puts + allow($stdout).to receive(:puts) + + # Track which files would be written to + written_files = [] + allow_any_instance_of(Pathname).to receive(:write) do |instance, content| + written_files << {path: instance.to_s, content: content} + end + + # Run the task + Rake.application.invoke_task "support_table_data:add_yard_docs" + + # Verify that at least one model had documentation added + expect(written_files).not_to be_empty + expect($stdout).to have_received(:puts).at_least(:once) + + # Verify the written content looks correct (check one example) + color_write = written_files.find { |f| f[:path].include?("color.rb") } + expect(color_write).not_to be_nil + expect(color_write[:content]).to include("# Begin YARD docs for support_table_data") + expect(color_write[:content]).to include("@!method self.red") + end + end end diff --git a/test_app/.gitignore b/test_app/.gitignore new file mode 100644 index 0000000..1b55771 --- /dev/null +++ b/test_app/.gitignore @@ -0,0 +1,4 @@ +log/*.log +tmp/ +doc/ +db/*.sqlite3 diff --git a/test_app/Gemfile b/test_app/Gemfile new file mode 100644 index 0000000..7164e9c --- /dev/null +++ b/test_app/Gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +gem "rails", "~> 8.1.1" + +gem "sqlite3", "~> 2.9.0" + +gem "support_table_data", path: ".." diff --git a/test_app/Rakefile b/test_app/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/test_app/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/test_app/app/models/status.rb b/test_app/app/models/status.rb new file mode 100644 index 0000000..3d3ebae --- /dev/null +++ b/test_app/app/models/status.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Status < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :code + add_support_table_data "statuses.yml" + named_instance_attribute_helpers :name +end diff --git a/test_app/bin/rails b/test_app/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/test_app/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/test_app/config.ru b/test_app/config.ru new file mode 100644 index 0000000..c7766f3 --- /dev/null +++ b/test_app/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +# run Rails.application +# Rails.application.load_server diff --git a/test_app/config/application.rb b/test_app/config/application.rb new file mode 100644 index 0000000..680a1b4 --- /dev/null +++ b/test_app/config/application.rb @@ -0,0 +1,42 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +# require "active_model/railtie" +# require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +# require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +# require "action_view/railtie" +# require "action_cable/engine" +# require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module TestApp + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.1 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + config.eager_load_paths << Rails.root.join("app", "configurations") + + # Don't generate system test files. + config.generators.system_tests = nil + end +end diff --git a/test_app/config/boot.rb b/test_app/config/boot.rb new file mode 100644 index 0000000..2820116 --- /dev/null +++ b/test_app/config/boot.rb @@ -0,0 +1,3 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. diff --git a/test_app/config/database.yml b/test_app/config/database.yml new file mode 100644 index 0000000..576bcf4 --- /dev/null +++ b/test_app/config/database.yml @@ -0,0 +1,3 @@ +development: + adapter: sqlite3 + database: db/development.sqlite3 diff --git a/test_app/config/environment.rb b/test_app/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/test_app/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test_app/config/environments/development.rb b/test_app/config/environments/development.rb new file mode 100644 index 0000000..cdc965b --- /dev/null +++ b/test_app/config/environments/development.rb @@ -0,0 +1,11 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false +end diff --git a/test_app/db/migrate/20260103060951_create_status.rb b/test_app/db/migrate/20260103060951_create_status.rb new file mode 100644 index 0000000..eee26f5 --- /dev/null +++ b/test_app/db/migrate/20260103060951_create_status.rb @@ -0,0 +1,8 @@ +class CreateStatus < ActiveRecord::Migration[8.1] + def change + create_table :statuses do |t| + t.string :code, null: false, index: {unique: true} + t.string :name, null: false, index: {unique: true} + end + end +end diff --git a/test_app/db/schema.rb b/test_app/db/schema.rb new file mode 100644 index 0000000..18095ef --- /dev/null +++ b/test_app/db/schema.rb @@ -0,0 +1,20 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2026_01_03_060951) do + create_table "statuses", force: :cascade do |t| + t.string "code", null: false + t.string "name", null: false + t.index ["code"], name: "index_statuses_on_code", unique: true + t.index ["name"], name: "index_statuses_on_name", unique: true + end +end diff --git a/test_app/db/support_tables/statuses.yml b/test_app/db/support_tables/statuses.yml new file mode 100644 index 0000000..4377678 --- /dev/null +++ b/test_app/db/support_tables/statuses.yml @@ -0,0 +1,19 @@ +pending: + code: pending + name: Pending + +active: + code: active + name: Active + +completed: + code: completed + name: Completed + +canceled: + code: canceled + name: Canceled + +failed: + code: failed + name: Failed diff --git a/test_app/log/.keep b/test_app/log/.keep new file mode 100644 index 0000000..e69de29