From d1bbe90591ac762b0c34a60708455018bf6b0388 Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Tue, 25 Apr 2023 17:59:33 -0700 Subject: [PATCH 1/3] add named instance attribute helpers --- CHANGELOG.md | 7 +++ README.md | 16 +++++++ VERSION | 2 +- lib/support_table_data.rb | 84 ++++++++++++++++++++++++++------- spec/spec_helper.rb | 4 ++ spec/support_table_data_spec.rb | 11 +++++ 6 files changed, 105 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e7a01..af3d672 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,14 @@ 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.1.0 + +### Added + +- Additional helper methods defined on the class to expose the key values for named instances. + ## 1.0.0 ### Added + - Add SupportTableData concern to enable automatic syncing of data on support tables. diff --git a/README.md b/README.md index 358ada5..fd7e960 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,22 @@ status.completed? # status.id == 3 Helper methods will not override already defined methods on a model class. If a method is already defined, an `ArgumentError` will be raised. +You can also define helper methods for named instance attributes. + +```ruby +class Status < ApplicationRecord + include SupportTableData + + named_instance_attribute_helpers :id +end + +Status.pending_id # => 1 +Status.in_progress_id # => 2 +Status.completed_id # => 3 +``` + +These helper methods will return the hard coded values from the data file and will not query the database. + ### 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/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/lib/support_table_data.rb b/lib/support_table_data.rb index d37991f..238341c 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -64,6 +64,21 @@ def add_support_table_data(data_file_path) define_support_table_named_instances end + # Add class methods to get attribute values for named instances. The methods will be named + # like `#{instance_name}_#{attribute_name}`. For example, if the name is "active" and the + # attribute is "id", then the method will be "active_id" and you can call + # `Model.active_id` to get the value. + # + # @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 + define_support_table_named_instances + end + # Get the data for the support table from the data files. # # @return [Array] List of attributes for all records in the data files. @@ -135,35 +150,52 @@ def protected_instance?(instance) def define_support_table_named_instances @support_table_data_files ||= [] @support_table_instance_names ||= Set.new - key_attribute = (support_table_key_attribute || primary_key).to_s @support_table_data_files.each do |file_path| data = support_table_parse_data_file(file_path) - if data.is_a?(Hash) - data.each do |key, attributes| - method_name = key.to_s.freeze - next if method_name.start_with?("_") + next unless data.is_a?(Hash) - unless attributes.is_a?(Hash) - raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; value must be a Hash") - end + data.each do |name, attributes| + define_support_table_named_instance_methods(name, attributes) + end + end + end - unless method_name.match?(/\A[a-z][a-z0-9_]+\z/) - raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; name contains illegal characters") - end + def define_support_table_named_instance_methods(name, attributes) + method_name = name.to_s.freeze + return if method_name.start_with?("_") - unless @support_table_instance_names.include?(method_name) - @support_table_instance_names << method_name - key_value = attributes[key_attribute] - define_support_table_instance_helper(method_name, key_attribute, key_value) - define_support_table_predicates_helper("#{method_name}?", key_attribute, key_value) - end - end + unless attributes.is_a?(Hash) + raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; value must be a Hash") + end + + unless method_name.match?(/\A[a-z][a-z0-9_]+\z/) + raise ArgumentError.new("Cannot define named instance #{method_name} on #{name}; name contains illegal characters") + end + + key_attribute = (support_table_key_attribute || primary_key).to_s + key_value = attributes[key_attribute] + + 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 + end + + if defined?(@support_table_attribute_helpers) + @support_table_attribute_helpers.each do |attribute_name, defined_methods| + attribute_method_name = "#{method_name}_#{attribute_name}" + next if defined_methods.include?(attribute_method_name) + + define_support_table_instance_attribute_helper(attribute_method_name, attributes[attribute_name]) + defined_methods << attribute_method_name end end 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 @@ -175,7 +207,23 @@ def self.#{method_name} RUBY 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 + + class_eval <<~RUBY, __FILE__, __LINE__ + 1 + def self.#{method_name} + #{attribute_value.inspect} + end + RUBY + 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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0fad077..befb4d4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -72,8 +72,12 @@ class Group < ActiveRecord::Base 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/support_table_data_spec.rb b/spec/support_table_data_spec.rb index 8e07947..d2d3ee4 100644 --- a/spec/support_table_data_spec.rb +++ b/spec/support_table_data_spec.rb @@ -139,6 +139,17 @@ end end + describe "named_instance_attribute_helpers" do + it "adds helper methods for each attribute on named instances" do + expect(Group.primary_group_id).to eq 1 + expect(Group.secondary_group_id).to eq 2 + expect(Group.gray_group_id).to eq 3 + expect(Group.primary_name).to eq "primary" + expect(Group.secondary_name).to eq "secondary" + expect(Group.gray_name).to eq "gray" + end + end + describe "protected_instance?" do it "returns true if the instance came from a data file" do red = Color.new From 0270bdfdee078d5d8e73350d236c6709cbeb6c20 Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Tue, 25 Apr 2023 18:24:01 -0700 Subject: [PATCH 2/3] clarify docs --- CHANGELOG.md | 2 +- README.md | 5 ++--- lib/support_table_data.rb | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3d672..5563753 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Additional helper methods defined on the class to expose the key values for named instances. +- Helper methods can defined on the class to expose attributes for named instances without requiring a database connection. ## 1.0.0 diff --git a/README.md b/README.md index fd7e960..c486fbb 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ status.completed? # status.id == 3 Helper methods will not override already defined methods on a model class. If a method is already defined, an `ArgumentError` will be raised. -You can also define helper methods for named instance attributes. +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 @@ -118,8 +119,6 @@ Status.in_progress_id # => 2 Status.completed_id # => 3 ``` -These helper methods will return the hard coded values from the data file and will not query the database. - ### 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/lib/support_table_data.rb b/lib/support_table_data.rb index 238341c..edf0814 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -64,7 +64,7 @@ def add_support_table_data(data_file_path) define_support_table_named_instances end - # Add class methods to get attribute values for named instances. The methods will be named + # Add class methods to get attributes for named instances. The methods will be named # like `#{instance_name}_#{attribute_name}`. For example, if the name is "active" and the # attribute is "id", then the method will be "active_id" and you can call # `Model.active_id` to get the value. From cc5d68b0397c6a73c809b1617c059b5d8b2eeb2c Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Wed, 26 Apr 2023 17:02:01 -0700 Subject: [PATCH 3/3] expose attribute helper names --- lib/support_table_data.rb | 9 +++++++++ spec/support_table_data_spec.rb | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/lib/support_table_data.rb b/lib/support_table_data.rb index edf0814..63445c0 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -79,6 +79,15 @@ def named_instance_attribute_helpers(*attributes) define_support_table_named_instances end + # Get the names of any named instance attribute helpers that have been defined + # with `named_instance_attribute_helpers`. + # + # @return [Array] 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] List of attributes for all records in the data files. diff --git a/spec/support_table_data_spec.rb b/spec/support_table_data_spec.rb index d2d3ee4..ebec39f 100644 --- a/spec/support_table_data_spec.rb +++ b/spec/support_table_data_spec.rb @@ -148,6 +148,11 @@ expect(Group.secondary_name).to eq "secondary" expect(Group.gray_name).to eq "gray" end + + it "can get a list of the defined attribute helpers" do + expect(Group.support_table_attribute_helpers).to match_array ["group_id", "name"] + expect(Color.support_table_attribute_helpers).to match_array [] + end end describe "protected_instance?" do