diff --git a/CHANGELOG.md b/CHANGELOG.md index aebc383..b71c2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ -## [Unreleased] +## [0.4.0] - 2026-09-25 - Show entities whose record has been deleted instead of raising in the UI - Add `Flipside::Flippable` with `flipside_entity` and `flipside_role`, so models can register themselves (listed in `Flipside.flippables`); entities of a destroyed record are removed - Add `Flipside.prune_orphaned_entities` - Registering the same role twice no longer lists it twice +- Display STI subclass entities through their own or their base class's registration +- Verify `Flipside.flippables` at boot when a Rails app eager loads ### Deprecated - `Flipside.register_entity` and `Flipside.register_role`, in favour of `flipside_entity` and `flipside_role` from `Flipside::Flippable`. They will be removed in 1.0. diff --git a/Gemfile.lock b/Gemfile.lock index 99ca741..51bc284 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - flipside (0.3.4) + flipside (0.4.0) activerecord (>= 6.0) roda tilt diff --git a/README.md b/README.md index 8e9bf70..e8855aa 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ Entities can be added to a feature by searching for records. ![Add an entity](/add_entity.png) -`flipside_entity` should be called in each class that may be used as a feature enabler. It also removes the entities of a record when it is destroyed. +`flipside_entity` should be called in each class that may be used as a feature enabler. It also removes the entities of a record when it is destroyed. Note that this includes soft deletion that runs destroy callbacks (e.g. paranoia), so a restored record comes back without its features. Soft deletion that skips them (e.g. discard) keeps the entities. ```ruby class User < ApplicationRecord include Flipside::Flippable diff --git a/lib/flipside/config/entities.rb b/lib/flipside/config/entities.rb index efabf2d..f8b0791 100644 --- a/lib/flipside/config/entities.rb +++ b/lib/flipside/config/entities.rb @@ -48,8 +48,8 @@ def display_entity(entity) if record.nil? "#{label} (deleted)" - elsif registered_entities.key?(record.class.to_s) - display_flippable(record) + elsif (registration = registration_for(record.class)) + registration.display(record) else label end @@ -58,9 +58,23 @@ def display_entity(entity) private def display_flippable(record) - registered_entities - .fetch(record.class.to_s) - .display(record) + registration = registration_for(record.class) + raise KeyError, "#{record.class} is not registered with Flipside" unless registration + + registration.display(record) + end + + # The registration of the class or its closest registered superclass, so + # an STI subclass is displayed through the base class's registration + # unless it has one of its own. + def registration_for(klass) + klass.ancestors.each do |ancestor| + next unless ancestor.is_a?(Class) + + registration = registered_entities[ancestor.name] + return registration if registration + end + nil end def registered_entities diff --git a/lib/flipside/config/flippables.rb b/lib/flipside/config/flippables.rb index 94bc8c7..f7fe939 100644 --- a/lib/flipside/config/flippables.rb +++ b/lib/flipside/config/flippables.rb @@ -14,6 +14,13 @@ def register_flippable(class_name) # :nodoc: registered_flippables << class_name.to_s end + # Raises unless Flipside.flippables and the classes calling the + # Flipside::Flippable macros agree. Runs whenever the UI reads the + # registry, and at boot in an app that eager loads (see Railtie). + def verify_flippables! # :nodoc: + load_flippables + end + private def load_flippables diff --git a/lib/flipside/railtie.rb b/lib/flipside/railtie.rb index 5571427..d548ca3 100644 --- a/lib/flipside/railtie.rb +++ b/lib/flipside/railtie.rb @@ -1,9 +1,18 @@ module Flipside - # Hands Flipside's deprecator to the host app, so its deprecation settings - # (log, raise, silence) apply to Flipside's warnings too. + # Wires Flipside into a Rails app. Only loaded when Rails is present. class Railtie < ::Rails::Railtie + # Hands Flipside's deprecator to the app, so its deprecation settings (log, + # raise, silence) apply to Flipside's warnings too. initializer "flipside.deprecator" do |app| app.deprecators[:flipside] = Flipside.deprecator if app.respond_to?(:deprecators) end + + # Eager loading runs before after_initialize, so every class calling the + # macros has registered by now. Checking here fails boot, and so CI, for a + # misconfigured list instead of the first visit to the UI. Without eager + # loading the check would only see whatever happens to be loaded. + config.after_initialize do |app| + Flipside.verify_flippables! if app.config.eager_load + end end end diff --git a/lib/flipside/version.rb b/lib/flipside/version.rb index ba0b212..93128bc 100644 --- a/lib/flipside/version.rb +++ b/lib/flipside/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Flipside - VERSION = "0.3.4" + VERSION = "0.4.0" end diff --git a/spec/flipside/flippable_spec.rb b/spec/flipside/flippable_spec.rb index 0340135..e1759b3 100644 --- a/spec/flipside/flippable_spec.rb +++ b/spec/flipside/flippable_spec.rb @@ -75,6 +75,22 @@ def define_user(&block) end end + describe "Flipside.verify_flippables!" do + it "passes when the list matches the classes using the macros" do + Flipside.flippables = ["User"] + define_user { flipside_role(:admin?) } + + expect { Flipside.verify_flippables! }.not_to raise_error + end + + it "raises for a class using the macros that is not listed" do + define_user { flipside_role(:admin?) } + + expect { Flipside.verify_flippables! } + .to raise_error(Flipside::Error, "User must be listed in Flipside.flippables") + end + end + it "registers without deprecation warnings" do expect(Flipside.deprecator).not_to receive(:warn) Flipside.flippables = ["User"] diff --git a/spec/flipside_spec.rb b/spec/flipside_spec.rb index ebad6b7..2f153e6 100644 --- a/spec/flipside_spec.rb +++ b/spec/flipside_spec.rb @@ -384,6 +384,19 @@ def admin? expect(Flipside.display_entity(entity.reload)).to eq("User ##{user.id} (deleted)") end + it "displays an STI subclass through the registration of its base class" do + ActiveRecord::Base.connection.add_column(:users, :type, :string) + User.reset_column_information + stub_const("Admin", Class.new(User)) + Flipside.add_registered_entity(class_name: "User", search_by: nil, display_as: :name) + feature = Feature.create!(name: "some_feature") + admin = Admin.create!(name: "John Doe") + entity = Entity.create!(feature:, flippable: admin) + + expect(Flipside.display_entity(entity.reload)).to eq("John Doe") + expect(Flipside.display_entity(admin)).to eq("John Doe") + end + it "displays an entity whose flippable is an STI subclass" do ActiveRecord::Base.connection.add_column(:users, :type, :string) User.reset_column_information