Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow safe reloads of the component registry
Browse files Browse the repository at this point in the history
This adds a new `ComponentRegistry` class that allows:
- setting component classes as strings (to allow for easy reloading)
- getting nice errors when requesting components that don't exist
- getting nice errors when setting a component to a non-existing class

It also moves some of the logic from `SolidusAdmin::Configuration` into
a separate class.

I've decided not to add memoization here as I'm not sure this would make
a giant difference performance-wise, and also the Rails Guide on
Reloading constants very clearly says:

```
Bottom line: do not cache reloadable classes or modules.
```
mamhoff committed Jun 11, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent dc780f6 commit 83ef1ac
Showing 5 changed files with 80 additions and 28 deletions.
7 changes: 1 addition & 6 deletions admin/docs/customizing_components.md
Original file line number Diff line number Diff line change
@@ -100,14 +100,9 @@ end
If you need more control, you can explicitly register your component in the
Solidus Admin container instead of using an implicit path:

> ⓘ Right now, that will raise an error when the application is reloaded. We
> need to fix it.
```ruby
# config/initalizers/solidus_admin.rb
Rails.application.config.to_prepare do
SolidusAdmin::Config.components['ui/button'] = MyApplication::Button::Component
end
SolidusAdmin::Config.components['ui/button'] = "MyApplication::Button::Component"
```

### Tweaking a component
34 changes: 34 additions & 0 deletions admin/lib/solidus_admin/component_registry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module SolidusAdmin
class ComponentRegistry
ComponentNotFoundError = Class.new(NameError)

def initialize
@names = {}
end

def []=(key, value)
@names[key] = value
end

def [](key)
(@names[key] || "solidus_admin/#{key}/component".classify).constantize
rescue NameError => error
if @names[key]
raise error
else
prefix = "#{SolidusAdmin::Configuration::ENGINE_ROOT}/app/components/solidus_admin/"
suffix = "/component.rb"
dictionary = Dir["#{prefix}**#{suffix}"].map { _1.delete_prefix(prefix).delete_suffix(suffix) }
corrections = DidYouMean::SpellChecker.new(dictionary: dictionary).correct(key.to_s)

raise ComponentNotFoundError.new(
"Unknown component #{key}#{DidYouMean.formatter.message_for(corrections)}",
key.classify,
receiver: ::SolidusAdmin
)
end
end
end
end
21 changes: 2 additions & 19 deletions admin/lib/solidus_admin/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# frozen_string_literal: true

require 'spree/preferences/configuration'
require 'solidus_admin/component_registry'

module SolidusAdmin
# Configuration for the admin interface.
#
# Ensure requiring this file after the Rails application has been created,
# as some defaults depend on the application context.
class Configuration < Spree::Preferences::Configuration
ComponentNotFoundError = Class.new(NameError)
ENGINE_ROOT = File.expand_path("#{__dir__}/../..")

# Path to the logo used in the admin interface.
@@ -180,24 +180,7 @@ def import_menu_items_from_backend!
end

def components
@components ||= Hash.new do |_h, k|
const_name = "solidus_admin/#{k}/component".classify

unless Object.const_defined?(const_name)
prefix = "#{ENGINE_ROOT}/app/components/solidus_admin/"
suffix = "/component.rb"
dictionary = Dir["#{prefix}**#{suffix}"].map { _1.delete_prefix(prefix).delete_suffix(suffix) }
corrections = DidYouMean::SpellChecker.new(dictionary: dictionary).correct(k.to_s)

raise ComponentNotFoundError.new(
"Unknown component #{k}#{DidYouMean.formatter.message_for(corrections)}",
k.classify,
receiver: ::SolidusAdmin
)
end

const_name.constantize
end
@components = ComponentRegistry.new
end

# The method used to authenticate the user in the admin interface, it's expected to redirect the user to the login method
42 changes: 42 additions & 0 deletions admin/spec/solidus_admin/component_registry_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe SolidusAdmin::ComponentRegistry do
let(:registry) { described_class.new }
let(:key) { "ui/button" }

subject { registry[key] }

context "with a default class" do
it { is_expected.to eq(SolidusAdmin::UI::Button::Component) }
end

context "with a spelling mistake" do
let(:key) { "ui/buton" }

it "raises an understandable error" do
expect { subject }.to raise_error("Unknown component ui/buton\nDid you mean? ui/button")
end
end

context "with a custom class" do
before do
# Using an existing class here so I don't have to define a new one.
# Extensions that use this should use their own.
registry["ui/button"] = "SolidusAdmin::UI::Panel::Component"
end

it { is_expected.to eq(SolidusAdmin::UI::Panel::Component) }
end

context "with a custom class with a spelling mistake" do
before do
registry["ui/button"] = "DoesNotExistClass"
end

it "raises an NameError" do
expect { subject }.to raise_error("uninitialized constant DoesNotExistClass")
end
end
end
4 changes: 1 addition & 3 deletions legacy_promotions/lib/solidus_legacy_promotions/engine.rb
Original file line number Diff line number Diff line change
@@ -31,9 +31,7 @@ class Engine < ::Rails::Engine

initializer "solidus_legacy_promotions.add_admin_order_index_component" do
if SolidusSupport.admin_available?
config.to_prepare do
SolidusAdmin::Config.components["orders/index"] = SolidusLegacyPromotions::Orders::Index::Component
end
SolidusAdmin::Config.components["orders/index"] = "SolidusLegacyPromotions::Orders::Index::Component"
end
end

0 comments on commit 83ef1ac

Please sign in to comment.