From 797e5f391371790a3e7cc26fc9793e38943d311c Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Thu, 23 May 2024 20:09:00 -0700 Subject: [PATCH] Added named instance helpers to make loading associated data easier. --- .github/workflows/regression_test.yml | 29 ---------- CHANGELOG.md | 7 +++ README.md | 52 ++++++++++++++++- VERSION | 2 +- lib/support_table_data.rb | 83 +++++++++++++++++---------- spec/support_table_data_spec.rb | 28 ++++++++- 6 files changed, 140 insertions(+), 61 deletions(-) delete mode 100644 .github/workflows/regression_test.yml diff --git a/.github/workflows/regression_test.yml b/.github/workflows/regression_test.yml deleted file mode 100644 index d186fea..0000000 --- a/.github/workflows/regression_test.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Regression Test -on: - workflow_dispatch: - schedule: - - cron: "0 15 * * 1" -env: - BUNDLE_CLEAN: "true" - BUNDLE_PATH: vendor/bundle - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 -jobs: - specs: - name: Run specs - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Ruby ${{ matrix.ruby }} - uses: ruby/setup-ruby@v1 - with: - ruby-version: ruby - - name: Install bundler - run: | - bundle update - - name: Run specs - run: | - bundle exec rake spec diff --git a/CHANGELOG.md b/CHANGELOG.md index d079516..6cb7b54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 7189c50..9fd3ce6 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. @@ -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 diff --git a/VERSION b/VERSION index 45a1b3f..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.2 +1.2.0 diff --git a/lib/support_table_data.rb b/lib/support_table_data.rb index 33df693..15f60d6 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -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. @@ -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 @@ -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 @@ -84,7 +99,6 @@ def named_instance_attribute_helpers(*attributes) # # @return [Array] List of attribute names. def support_table_attribute_helpers - @support_table_attribute_helpers ||= {} @support_table_attribute_helpers.keys end @@ -92,7 +106,6 @@ def support_table_attribute_helpers # # @return [Array] 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 @@ -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] 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| @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/spec/support_table_data_spec.rb b/spec/support_table_data_spec.rb index 4a66d4d..7f5b3a3 100644 --- a/spec/support_table_data_spec.rb +++ b/spec/support_table_data_spec.rb @@ -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 @@ -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 @@ -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