diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 4756930..0c3a4aa 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -28,13 +28,15 @@ jobs: - ruby: "ruby" standardrb: true - ruby: "3.4" - appraisal: "activerecord_8" + appraisal: "activerecord_8.0" - ruby: "3.2" - appraisal: "activerecord_7" + appraisal: "activerecord_7.2" + - ruby: "3.0" + appraisal: "activerecord_7.1" - ruby: "2.7" - appraisal: "activerecord_6" + appraisal: "activerecord_7.0" - ruby: "2.5" - appraisal: "activerecord_5" + appraisal: "activerecord_6.1" steps: - uses: actions/checkout@v4 - name: Set up Ruby ${{ matrix.ruby }} diff --git a/Appraisals b/Appraisals index f73ff29..52818d3 100644 --- a/Appraisals +++ b/Appraisals @@ -1,22 +1,30 @@ # frozen_string_literal: true -appraise "activerecord_8" do - gem "activerecord", "~> 8.0" +appraise "activerecord_8.0" do + gem "activerecord", "~> 8.0.0" gem "sqlite3", "~> 2.5.0" end -appraise "activerecord_7" do - gem "activerecord", "~> 7.0" +appraise "activerecord_7.2" do + gem "activerecord", "~> 7.0.0" gem "sqlite3", "~> 1.4.0" + gem "concurrent-ruby", "1.3.4" end -appraise "activerecord_6" do - gem "activerecord", "~> 6.0" +appraise "activerecord_7.1" do + gem "activerecord", "~> 7.0.0" gem "sqlite3", "~> 1.4.0" gem "concurrent-ruby", "1.3.4" end -appraise "activerecord_5" do - gem "activerecord", "~> 5.0" - gem "sqlite3", "~> 1.3.0" +appraise "activerecord_7.0" do + gem "activerecord", "~> 7.0.0" + gem "sqlite3", "~> 1.4.0" + gem "concurrent-ruby", "1.3.4" +end + +appraise "activerecord_6.1" do + gem "activerecord", "~> 6.1.0" + gem "sqlite3", "~> 1.4.0" + gem "concurrent-ruby", "1.3.4" end diff --git a/CHANGELOG.md b/CHANGELOG.md index d3317c5..cd2e099 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ 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.0 + +### Fixed + +- Honor single table inheritance class when creating new records in the database. This fixes issues where validations and callbacks on subclasses could be skipped when creating new records. + +### Removed + +- Removed support for ActiveRecord versions prior to 6.1. + ## 1.3.1 ### Added diff --git a/VERSION b/VERSION index 3a3cd8c..88c5fb8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 +1.4.0 diff --git a/gemfiles/activerecord_6.gemfile b/gemfiles/activerecord_6.1.gemfile similarity index 89% rename from gemfiles/activerecord_6.gemfile rename to gemfiles/activerecord_6.1.gemfile index fd7b4c5..a246139 100644 --- a/gemfiles/activerecord_6.gemfile +++ b/gemfiles/activerecord_6.1.gemfile @@ -10,7 +10,7 @@ gem "standard", "~>1.0" gem "pry-byebug" gem "yard" gem "csv" -gem "activerecord", "~> 6.0" +gem "activerecord", "~> 6.1.0" gem "concurrent-ruby", "1.3.4" gemspec path: "../" diff --git a/gemfiles/activerecord_7.gemfile b/gemfiles/activerecord_7.0.gemfile similarity index 78% rename from gemfiles/activerecord_7.gemfile rename to gemfiles/activerecord_7.0.gemfile index d347948..f25d6c6 100644 --- a/gemfiles/activerecord_7.gemfile +++ b/gemfiles/activerecord_7.0.gemfile @@ -10,6 +10,7 @@ gem "standard", "~>1.0" gem "pry-byebug" gem "yard" gem "csv" -gem "activerecord", "~> 7.0" +gem "activerecord", "~> 7.0.0" +gem "concurrent-ruby", "1.3.4" gemspec path: "../" diff --git a/gemfiles/activerecord_5.gemfile b/gemfiles/activerecord_7.1.gemfile similarity index 69% rename from gemfiles/activerecord_5.gemfile rename to gemfiles/activerecord_7.1.gemfile index db9b0bd..f25d6c6 100644 --- a/gemfiles/activerecord_5.gemfile +++ b/gemfiles/activerecord_7.1.gemfile @@ -4,12 +4,13 @@ source "https://rubygems.org" gem "rspec", "~> 3.0" gem "rake" -gem "sqlite3", "~> 1.3.0" +gem "sqlite3", "~> 1.4.0" gem "appraisal" gem "standard", "~>1.0" gem "pry-byebug" gem "yard" gem "csv" -gem "activerecord", "~> 5.0" +gem "activerecord", "~> 7.0.0" +gem "concurrent-ruby", "1.3.4" gemspec path: "../" diff --git a/gemfiles/activerecord_7.2.gemfile b/gemfiles/activerecord_7.2.gemfile new file mode 100644 index 0000000..f25d6c6 --- /dev/null +++ b/gemfiles/activerecord_7.2.gemfile @@ -0,0 +1,16 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rspec", "~> 3.0" +gem "rake" +gem "sqlite3", "~> 1.4.0" +gem "appraisal" +gem "standard", "~>1.0" +gem "pry-byebug" +gem "yard" +gem "csv" +gem "activerecord", "~> 7.0.0" +gem "concurrent-ruby", "1.3.4" + +gemspec path: "../" diff --git a/gemfiles/activerecord_8.gemfile b/gemfiles/activerecord_8.0.gemfile similarity index 88% rename from gemfiles/activerecord_8.gemfile rename to gemfiles/activerecord_8.0.gemfile index 0b68d6b..b1709a1 100644 --- a/gemfiles/activerecord_8.gemfile +++ b/gemfiles/activerecord_8.0.gemfile @@ -10,6 +10,6 @@ gem "standard", "~>1.0" gem "pry-byebug" gem "yard" gem "csv" -gem "activerecord", "~> 8.0" +gem "activerecord", "~> 8.0.0" gemspec path: "../" diff --git a/lib/support_table_data.rb b/lib/support_table_data.rb index 85f7cc4..f002120 100644 --- a/lib/support_table_data.rb +++ b/lib/support_table_data.rb @@ -56,7 +56,9 @@ def sync_table_data! end canonical_data.each_value do |attributes| - record = new + class_name = attributes[inheritance_column] + klass = class_name ? sti_class_for(class_name) : self + record = klass.new attributes.each do |name, value| record.send(:"#{name}=", value) if record.respond_to?(:"#{name}=", true) end diff --git a/spec/data/polygons.yml b/spec/data/polygons.yml new file mode 100644 index 0000000..da06802 --- /dev/null +++ b/spec/data/polygons.yml @@ -0,0 +1,13 @@ +triangle: + name: Triangle + type: Triangle + side_count: 3 + +rectangle: + name: Rectangle + type: Rectangle + side_count: 4 + +pentagon: + name: Pentagon + side_count: 5 diff --git a/spec/models.rb b/spec/models.rb new file mode 100644 index 0000000..6fed774 --- /dev/null +++ b/spec/models.rb @@ -0,0 +1,185 @@ +ActiveRecord::Base.connection.tap do |connection| + connection.create_table(:colors) do |t| + t.string :name, index: {unique: true} + t.integer :value + t.string :comment + t.integer :group_id + t.integer :hue_id + end + + connection.create_table(:groups, primary_key: :group_id) do |t| + t.string :name, index: {unique: true} + t.timestamps + end + + connection.create_table(:hues) do |t| + t.string :name, index: {unique: true} + t.integer :parent_id + end + + connection.create_table(:shades) do |t| + t.string :name + end + + connection.create_table(:shade_hues) do |t| + t.integer :shade_id + t.integer :hue_id + end + + connection.create_table(:things) do |t| + t.string :name + t.integer :color_id + t.integer :shade_id + end + + connection.create_table(:aliases) do |t| + t.string :name + t.integer :color_id + end + + connection.create_table(:invalids) do |t| + t.string :name + end + + connection.create_table(:polygons) do |t| + t.string :name + t.string :type + t.integer :side_count + end +end + +class Color < ActiveRecord::Base + include SupportTableData + + self.support_table_data_directory = File.join(__dir__, "data", "colors") + add_support_table_data "named_colors.yml" + add_support_table_data "named_colors.json" + add_support_table_data "colors.yml" + add_support_table_data File.join(__dir__, "data", "colors", "colors.json") + add_support_table_data "colors.csv" + + belongs_to :group + belongs_to :hue + has_many :things + has_many :shades, through: :things + has_many :aliases, autosave: true + + # Intentionally invalid association + belongs_to :non_existent, class_name: "NonExistent" + + validates_uniqueness_of :name + + def group_name=(value) + self.group = Group.named_instance(value) + end + + def hue_name=(value) + self.hue = Hue.find_by!(name: value) + end + + def alias_names=(names) + self.aliases = names.map { |name| Alias.find_or_initialize_by(name: name) } + end + + private + + def hex=(value) + self.value = value.to_i(16) + end +end + +class Alias < ActiveRecord::Base + belongs_to :color + + validates_uniqueness_of :name +end + +class Group < ActiveRecord::Base + include SupportTableData + + 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 + +class Hue < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "hues.yml" + + belongs_to :parent, class_name: "Hue", optional: true + + validates_uniqueness_of :name + + def parent_name=(value) + self.parent = Hue.find_by!(name: value) + end + + has_many :shade_hues + has_many :shades, through: :shade_hues, autosave: true + + support_table_dependency "Shade" + + def shade_names=(names) + self.shades = Shade.where(name: names) + end +end + +class Shade < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "shades.yml" + + validates_uniqueness_of :name + + has_many :shade_hues + has_many :hues, through: :shade_hues +end + +class ShadeHue < ActiveRecord::Base + belongs_to :shade + belongs_to :hue +end + +class Thing < ActiveRecord::Base + belongs_to :color + belongs_to :shade +end + +class Invalid < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + def already_defined? + true + end +end + +class Polygon < ActiveRecord::Base + include SupportTableData + + self.support_table_key_attribute = :name + + add_support_table_data "polygons.yml" + + validates :name, uniqueness: true +end + +class Triangle < Polygon + validates :side_count, numericality: {equal_to: 3} +end + +class Rectangle < Polygon + validates :side_count, numericality: {equal_to: 4} +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 345a15f..fe9b1cd 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,167 +8,7 @@ SupportTableData.data_directory = File.join(__dir__, "data") -ActiveRecord::Base.connection.tap do |connection| - connection.create_table(:colors) do |t| - t.string :name, index: {unique: true} - t.integer :value - t.string :comment - t.integer :group_id - t.integer :hue_id - end - - connection.create_table(:groups, primary_key: :group_id) do |t| - t.string :name, index: {unique: true} - t.timestamps - end - - connection.create_table(:hues) do |t| - t.string :name, index: {unique: true} - t.integer :parent_id - end - - connection.create_table(:shades) do |t| - t.string :name - end - - connection.create_table(:shade_hues) do |t| - t.integer :shade_id - t.integer :hue_id - end - - connection.create_table(:things) do |t| - t.string :name - t.integer :color_id - t.integer :shade_id - end - - connection.create_table(:aliases) do |t| - t.string :name - t.integer :color_id - end - - connection.create_table(:invalids) do |t| - t.string :name - end -end - -class Color < ActiveRecord::Base - include SupportTableData - - self.support_table_data_directory = File.join(__dir__, "data", "colors") - add_support_table_data "named_colors.yml" - add_support_table_data "named_colors.json" - add_support_table_data "colors.yml" - add_support_table_data File.join(__dir__, "data", "colors", "colors.json") - add_support_table_data "colors.csv" - - belongs_to :group - belongs_to :hue - has_many :things - has_many :shades, through: :things - has_many :aliases, autosave: true - - # Intentionally invalid association - belongs_to :non_existent, class_name: "NonExistent" - - validates_uniqueness_of :name - - def group_name=(value) - self.group = Group.named_instance(value) - end - - def hue_name=(value) - self.hue = Hue.find_by!(name: value) - end - - def alias_names=(names) - self.aliases = names.map { |name| Alias.find_or_initialize_by(name: name) } - end - - private - - def hex=(value) - self.value = value.to_i(16) - end -end - -class Alias < ActiveRecord::Base - belongs_to :color - - validates_uniqueness_of :name -end - -class Group < ActiveRecord::Base - include SupportTableData - - 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 - -class Hue < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - add_support_table_data "hues.yml" - - belongs_to :parent, class_name: "Hue", optional: true - - validates_uniqueness_of :name - - def parent_name=(value) - self.parent = Hue.find_by!(name: value) - end - - has_many :shade_hues - has_many :shades, through: :shade_hues, autosave: true - - support_table_dependency "Shade" - - def shade_names=(names) - self.shades = Shade.where(name: names) - end -end - -class Shade < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - add_support_table_data "shades.yml" - - validates_uniqueness_of :name - - has_many :shade_hues - has_many :hues, through: :shade_hues -end - -class ShadeHue < ActiveRecord::Base - belongs_to :shade - belongs_to :hue -end - -class Thing < ActiveRecord::Base - belongs_to :color - belongs_to :shade -end - -class Invalid < ActiveRecord::Base - include SupportTableData - - self.support_table_key_attribute = :name - - def already_defined? - true - end -end +require_relative "models" RSpec.configure do |config| config.order = :random diff --git a/spec/support_table_data_spec.rb b/spec/support_table_data_spec.rb index 2292de0..e00dfa6 100644 --- a/spec/support_table_data_spec.rb +++ b/spec/support_table_data_spec.rb @@ -42,6 +42,19 @@ Color.sync_table_data! expect(Color.find_by(name: "Dark Gray").aliases.pluck(:name)).to match_array ["Gunmetal", "Charcoal"] end + + it "creates records with the correct single table inheritance type" do + Polygon.sync_table_data! + expect(Polygon.triangle).to be_a Triangle + expect(Polygon.rectangle).to be_a Rectangle + expect(Polygon.pentagon).to be_a Polygon + end + it "honors the single table inheritance column when creating new records" do + allow(Polygon).to receive(:support_table_data).and_return([ + {"name" => "Triangle", "type" => "Triangle", "side_count" => 4} + ]) + expect { Polygon.sync_table_data! }.to raise_error(ActiveRecord::RecordInvalid) + end end describe "sync_all!" do @@ -197,7 +210,7 @@ describe "support_table_classes" do it "gets a list of all loaded support table classes with dependencies listed first" do - expect(SupportTableData.support_table_classes).to eq [Shade, Group, Hue, Color, Invalid] + expect(SupportTableData.support_table_classes).to eq [Shade, Group, Hue, Color, Invalid, Polygon] end end diff --git a/support_table_data.gemspec b/support_table_data.gemspec index e5976ac..e93c69d 100644 --- a/support_table_data.gemspec +++ b/support_table_data.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.5" - spec.add_dependency "activerecord" + spec.add_dependency "activerecord", ">= 6" spec.add_development_dependency "bundler" end