Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/continuous_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
140 changes: 140 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 0 additions & 17 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.4.0
1.4.1
15 changes: 15 additions & 0 deletions gemfiles/activerecord_8.1.gemfile
Original file line number Diff line number Diff line change
@@ -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: "../"
22 changes: 11 additions & 11 deletions lib/support_table_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
module SupportTableData
extend ActiveSupport::Concern

@data_directory = nil

included do
# Internal variables used for memoization.
@mutex = Mutex.new
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions lib/support_table_data/documentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

module SupportTableData
module Documentation
end
end

require_relative "documentation/source_file"
require_relative "documentation/yard_doc"
Loading