diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e7a01..5563753 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 + +- Helper methods can defined on the class to expose attributes for named instances without requiring a database connection. + ## 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..c486fbb 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,21 @@ 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. 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 + + named_instance_attribute_helpers :id +end + +Status.pending_id # => 1 +Status.in_progress_id # => 2 +Status.completed_id # => 3 +``` + ### 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..63445c0 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -64,6 +64,30 @@ def add_support_table_data(data_file_path) define_support_table_named_instances end + # 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. + # + # @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 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. @@ -135,35 +159,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 +216,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..ebec39f 100644 --- a/spec/support_table_data_spec.rb +++ b/spec/support_table_data_spec.rb @@ -139,6 +139,22 @@ 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 + + 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 it "returns true if the instance came from a data file" do red = Color.new