diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 1eaca470ccf..28b96933759 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -1428,6 +1428,10 @@ module Settings end def value + unless (override = resolve_value_override).nil? + return cast(override) + end + cast(@value) end @@ -1444,6 +1448,8 @@ module Settings end def writable? + return false if value_override? + if writable.respond_to?(:call) writable.call else @@ -1567,6 +1573,38 @@ module Settings @all ||= {} end + # Registers a value override block for a setting. The block is called + # whenever the setting's value or writability is evaluated. + # + # If the block returns a non-nil value, that value is used as the setting's + # value and the setting becomes non-writable. If the block returns nil, + # no override is applied. + # + # To override a setting with nil, return a callable: +-> { nil }+ + # + # @param name [Symbol] The setting name to override. + # @yield A block that returns the override value, or nil to skip. + # + # @example Force a setting to true when a condition is met + # Settings::Definition.add_value_override(:capture_external_links) do + # true if MyPlugin.active? + # end + def add_value_override(name, &block) + (value_overrides[name.to_sym] ||= []) << block + end + + def value_overrides + @value_overrides ||= {} + end + + def clear_value_overrides(name = nil) + if name + value_overrides.delete(name.to_sym) + else + @value_overrides = {} + end + end + private def file_config @@ -1760,6 +1798,18 @@ module Settings attr_accessor :serialized, :writable + def value_override? + !resolve_value_override.nil? + end + + def resolve_value_override + self.class.value_overrides[name.to_sym]&.each do |block| + result = block.call + return result unless result.nil? + end + nil + end + def cast(value) return nil if value.nil? diff --git a/spec/constants/settings/definition_spec.rb b/spec/constants/settings/definition_spec.rb index 9281def81fa..3c36291f813 100644 --- a/spec/constants/settings/definition_spec.rb +++ b/spec/constants/settings/definition_spec.rb @@ -1053,4 +1053,149 @@ RSpec.describe Settings::Definition, :settings_reset do end end end + + describe ".add_value_override" do + before do + described_class.add "bogus_override_test", + default: false, + format: :boolean + end + + after do + described_class.clear_value_overrides(:bogus_override_test) + end + + context "when the override block returns a non-nil value" do + before do + described_class.add_value_override(:bogus_override_test) do + true + end + end + + it "uses the returned value as the setting value" do + expect(described_class[:bogus_override_test].value).to be true + end + + it "marks the setting as non-writable" do + expect(described_class[:bogus_override_test]).not_to be_writable + end + end + + context "when the override block returns nil" do + before do + described_class.add_value_override(:bogus_override_test) do + nil + end + end + + it "uses the original default value" do + expect(described_class[:bogus_override_test].value).to be false + end + + it "keeps the setting writable" do + expect(described_class[:bogus_override_test]).to be_writable + end + end + + context "when the override block returns a callable" do + before do + described_class.add_value_override(:bogus_override_test) do + -> { nil } + end + end + + it "calls it to obtain the value, allowing override with nil" do + expect(described_class[:bogus_override_test].value).to be_nil + end + + it "marks the setting as non-writable" do + expect(described_class[:bogus_override_test]).not_to be_writable + end + end + + context "when the override is conditional" do + let(:condition) { true } + + before do + cond = condition + described_class.add_value_override(:bogus_override_test) do + true if cond + end + end + + context "when condition is met" do + let(:condition) { true } + + it "overrides the value" do + expect(described_class[:bogus_override_test].value).to be true + end + + it "is non-writable" do + expect(described_class[:bogus_override_test]).not_to be_writable + end + end + + context "when condition is not met" do + let(:condition) { false } + + it "uses the original value" do + expect(described_class[:bogus_override_test].value).to be false + end + + it "remains writable" do + expect(described_class[:bogus_override_test]).to be_writable + end + end + end + + context "with multiple override blocks" do + before do + described_class.add "bogus_multi_override_test", + default: "original", + format: :string + + described_class.add_value_override(:bogus_multi_override_test) do + nil + end + described_class.add_value_override(:bogus_multi_override_test) do + "from_second" + end + described_class.add_value_override(:bogus_multi_override_test) do + "from_third" + end + end + + after do + described_class.clear_value_overrides(:bogus_multi_override_test) + end + + it "uses the first block that returns a non-nil value" do + expect(described_class[:bogus_multi_override_test].value).to eq "from_second" + end + end + + context "when the setting is already non-writable" do + before do + described_class.add "bogus_non_writable_test", + default: "original", + format: :string, + writable: false + described_class.add_value_override(:bogus_non_writable_test) do + "overridden" + end + end + + after do + described_class.clear_value_overrides(:bogus_non_writable_test) + end + + it "overrides the value" do + expect(described_class[:bogus_non_writable_test].value).to eq "overridden" + end + + it "remains non-writable" do + expect(described_class[:bogus_non_writable_test]).not_to be_writable + end + end + end end