Skip to content

Commit

Permalink
Merge pull request #6 from bdurand/instance_data_accessors
Browse files Browse the repository at this point in the history
Added named instance helpers to make loading associated data easier.
  • Loading branch information
bdurand authored May 24, 2024
2 parents 3a488d4 + 797e5f3 commit 743d3b9
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 61 deletions.
29 changes: 0 additions & 29 deletions .github/workflows/regression_test.yml

This file was deleted.

7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ 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.2.0

### Added

- Added `named_instance` method to load a named instance from the database.
- Added class method `named_instance_data` to return attributes from the data files for a named instance.

## 1.1.2

### Fixed
Expand Down
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Support Table Data

[![Continuous Integration](https://github.com/bdurand/support_table_data/actions/workflows/continuous_integration.yml/badge.svg)](https://github.com/bdurand/support_table_data/actions/workflows/continuous_integration.yml)
[![Regression Test](https://github.com/bdurand/support_table_data/actions/workflows/regression_test.yml/badge.svg)](https://github.com/bdurand/support_table_data/actions/workflows/regression_test.yml)
[![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
[![Gem Version](https://badge.fury.io/rb/support_table_data.svg)](https://badge.fury.io/rb/support_table_data)

Expand Down Expand Up @@ -121,6 +120,55 @@ Status.in_progress_id # => 2
Status.completed_id # => 3
```

You can also use named instances to maintain associations between you models. In order to do this you'll need to implement a custom setter method.

```ruby
class Group < ApplicationRecord
include SupportTableData

has_many :statuses
end

class Status < ApplicationRecord
include SupportTableData

belongs_to :group

def group_name=(instance_name)
self.group = Group.named_instance(instance_name)
end
end
```

This then allows you to reference groups by instance name in the statuses.yml file:

```yaml
# groups.yml
not_done:
id: 1
name: Not Done

done:
id: 2
name: Done

# statuses.yml
pending:
id: 1
name: Pending
group_name: not_done

in_progress:
id: 2
name: In Progress
group_name: not_done

completed:
id: 3
name: Completed
group_name: done
```
### 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 Expand Up @@ -164,6 +212,8 @@ Loading data is done inside a database transaction. No changes will be persisted

You can synchronize the data in all models by calling `SupportTableData.sync_all!`. This method will discover all ActiveRecord models that include `SupportTableData` and synchronize each of them. (Note that there can be issues discovering all support table models in a Rails application if eager loading is turned off.) The discovery mechanism will try to detect unloaded classes by looking at the file names in the support table data directory so it's best to stick to standard Rails naming conventions for your data files.

The load order for models will resolve any dependencies between models. So if one model has a `belongs_to` association with another model, then the belongs to model will be loaded first.

You need to call `SupportTableData.sync_all!` when deploying your application. This gem includes a rake task `support_table_data:sync` that is suitable for hooking into deploy scripts. An easy way to hook it into a Rails application is by enhancing the `db:migrate` task so that the sync task runs immediately after database migrations are run. You can do this by adding code to a Rakefile in your application's `lib/tasks` directory:

```ruby
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.2.0
83 changes: 54 additions & 29 deletions lib/support_table_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,24 @@
module SupportTableData
extend ActiveSupport::Concern

module ClassMethods
included do
# Internal variables used for memoization.
@support_table_data_files = []
@support_table_attribute_helpers = {}
@support_table_instance_names = {}
@support_table_instance_keys = nil

# Define the attribute used as the key of the hash in the data files.
# This should be a value that never changes. By default the key attribute will be the id.
class_attribute :support_table_key_attribute, instance_accessor: false

# Define the directory where data files should be loaded from. This value will override the global
# value set by SupportTableData.data_directory. This is only used if relative paths are passed
# in to add_support_table_data.
class_attribute :support_table_data_directory, instance_accessor: false
end

class_methods do
# Synchronize the rows in the table with the values defined in the data files added with
# `add_support_table_data`. Note that rows will not be deleted if they are no longer in
# the data files.
Expand Down Expand Up @@ -58,7 +75,6 @@ def sync_table_data!
# this model or the global directory set with SupportTableData.data_directory.
# @return [void]
def add_support_table_data(data_file_path)
@support_table_data_files ||= []
root_dir = (support_table_data_directory || SupportTableData.data_directory || Dir.pwd)
@support_table_data_files << File.expand_path(data_file_path, root_dir)
define_support_table_named_instances
Expand All @@ -72,7 +88,6 @@ def add_support_table_data(data_file_path)
# @param attributes [String, Symbol] The names of the attributes to add helper methods for.
# @return [void]
def named_instance_attribute_helpers(*attributes)
@support_table_attribute_helpers ||= {}
attributes.flatten.collect(&:to_s).each do |attribute|
@support_table_attribute_helpers[attribute] = []
end
Expand All @@ -84,15 +99,13 @@ def named_instance_attribute_helpers(*attributes)
#
# @return [Array<String>] List of attribute names.
def support_table_attribute_helpers
@support_table_attribute_helpers ||= {}
@support_table_attribute_helpers.keys
end

# Get the data for the support table from the data files.
#
# @return [Array<Hash>] List of attributes for all records in the data files.
def support_table_data
@support_table_data_files ||= []
data = {}
key_attribute = (support_table_key_attribute || primary_key).to_s

Expand All @@ -114,19 +127,51 @@ def support_table_data
data.values
end

# Get the data for a named instances from the data files.
#
# @return [Hasn] Hash of named instance attributes.
def named_instance_data(name)
data = {}
name = name.to_s

@support_table_data_files.each do |data_file_path|
file_data = support_table_parse_data_file(data_file_path)
next unless file_data.is_a?(Hash)

file_data.each do |instance_name, attributes|
next unless name == instance_name.to_s
next unless attributes.is_a?(Hash)

data.merge!(attributes)
end
end

data
end

# Get the names of all named instances.
#
# @return [Array<String>] List of all instance names.
def instance_names
@support_table_instance_names ||= Set.new
@support_table_instance_names.to_a
@support_table_instance_names.keys
end

# Load a named instance from the database.
#
# @param instance_name [String, Symbol] The name of the instance to load as defined in the data files.
# @return [ActiveRecord::Base] The instance loaded from the database.
# @raise [ActiveRecord::RecordNotFound] If the instance does not exist.
def named_instance(instance_name)
key_attribute = (support_table_key_attribute || primary_key).to_s
instance_name = instance_name.to_s
find_by!(key_attribute => @support_table_instance_names[instance_name])
end

# Get the key values for all instances loaded from the data files.
#
# @return [Array] List of all the key attribute values.
def instance_keys
unless defined?(@support_table_instance_keys)
if @support_table_instance_keys.nil?
key_attribute = (support_table_key_attribute || primary_key).to_s
values = []
support_table_data.each do |attributes|
Expand Down Expand Up @@ -157,9 +202,6 @@ def protected_instance?(instance)
private

def define_support_table_named_instances
@support_table_data_files ||= []
@support_table_instance_names ||= Set.new

@support_table_data_files.each do |file_path|
data = support_table_parse_data_file(file_path)
next unless data.is_a?(Hash)
Expand Down Expand Up @@ -188,7 +230,7 @@ def define_support_table_named_instance_methods(name, attributes)
unless @support_table_instance_names.include?(method_name)
define_support_table_instance_helper(method_name, key_attribute, key_value)
define_support_table_predicates_helper("#{method_name}?", key_attribute, key_value)
@support_table_instance_names << method_name
@support_table_instance_names[method_name] = key_value
end

if defined?(@support_table_attribute_helpers)
Expand All @@ -203,8 +245,6 @@ def define_support_table_named_instance_methods(name, attributes)
end

def define_support_table_instance_helper(method_name, attribute_name, attribute_value)
return if @support_table_instance_names.include?("self.#{method_name}")

if respond_to?(method_name, true)
raise ArgumentError.new("Could not define support table helper method #{name}.#{method_name} because it is already a defined method")
end
Expand All @@ -217,8 +257,6 @@ def self.#{method_name}
end

def define_support_table_instance_attribute_helper(method_name, attribute_value)
return if @support_table_instance_names.include?("self.#{method_name}")

if respond_to?(method_name, true)
raise ArgumentError.new("Could not define support table helper method #{name}.#{method_name} because it is already a defined method")
end
Expand All @@ -231,8 +269,6 @@ def self.#{method_name}
end

def define_support_table_predicates_helper(method_name, attribute_name, attribute_value)
return if @support_table_instance_names.include?(method_name)

if method_defined?(method_name) || private_method_defined?(method_name)
raise ArgumentError.new("Could not define support table helper method #{name}##{method_name} because it is already a defined method")
end
Expand Down Expand Up @@ -268,17 +304,6 @@ def support_table_parse_data_file(file_path)
end
end

included do
# Define the attribute used as the key of the hash in the data files.
# This should be a value that never changes. By default the key attribute will be the id.
class_attribute :support_table_key_attribute, instance_accessor: false

# Define the directory where data files should be loaded from. This value will override the global
# value set by SupportTableData.data_directory. This is only used if relative paths are passed
# in to add_support_table_data.
class_attribute :support_table_data_directory, instance_accessor: false
end

class << self
# Specify the default directory for data files.
attr_writer :data_directory
Expand Down
28 changes: 27 additions & 1 deletion spec/support_table_data_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@
expect { Color.red }.to raise_error(ActiveRecord::RecordNotFound)
end

it "can load an instance by name" do
SupportTableData.sync_all!
expect(Color.named_instance("red")).to eq red
expect(Color.named_instance(:red)).to eq red
expect { Color.named_instance("pink") }.to raise_error(ActiveRecord::RecordNotFound)
end

it "defines predicate methods for comparing an attribute" do
SupportTableData.sync_all!
expect(red.red?).to eq true
Expand Down Expand Up @@ -185,7 +192,7 @@
end
end

describe "suppport_table_data" do
describe "support_table_data" do
it "returns an array with all the attributes" do
data = Color.support_table_data
expect(data.size).to eq 11
Expand All @@ -206,4 +213,23 @@
expect(data_1.map { |attributes| attributes.values.map(&:object_id) }.flatten).to_not match_array data_2.map { |attributes| attributes.values.map(&:object_id) }.flatten
end
end

describe "named_instance_data" do
it "returns a hash of the named instances" do
data = Color.named_instance_data("red")
expect(data["name"]).to eq "Red"
end

it "can use a symbol as the name" do
data = Color.named_instance_data(:red)
expect(data["name"]).to eq "Red"
end

it "returns a fresh copy every call" do
data_1 = Color.named_instance_data("red")
data_2 = Color.named_instance_data("red")
expect(data_1.object_id).to_not eq data_2.object_id
expect(data_1.values.map(&:object_id)).to_not match_array data_2.values.map(&:object_id)
end
end
end

0 comments on commit 743d3b9

Please sign in to comment.