Created
March 11, 2021 09:27
-
-
Save leoarnold/00b4fd3dbabadff50324400376fcf9ce to your computer and use it in GitHub Desktop.
Safe handling of Ruby constant redefinition (though you should not have to do it in the first place)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Constant | |
NAMESPACE_SEPARATOR = "::".freeze | |
module_function | |
def defined?(name) | |
Object.const_defined?(name) | |
end | |
def ensure(name, value) | |
return if self.defined?(name) && get(name) == value | |
redefine(name, value) | |
end | |
def get(name) | |
Object.const_get(name) | |
end | |
def redefine(name, value) | |
namespace, basename = unpack(name) | |
namespace.send(:remove_const, basename) if self.defined?(name) | |
namespace.const_set(basename, value) | |
end | |
def set(name, value) | |
fail NameError, "Constant '#{name}' already defined" if self.defined?(name) | |
redefine(name, value) | |
end | |
def split(name) | |
namespace, _, basename = name.to_s.rpartition(NAMESPACE_SEPARATOR) | |
[namespace.empty? ? nil : namespace, basename] | |
end | |
def unpack(name) | |
namespace, basename = split(name) | |
namespace = namespace.nil? ? Object : get(namespace) | |
[namespace, basename] | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "constant" | |
require "securerandom" | |
describe Constant do | |
before do | |
Object.send(:remove_const, :FOOBAR) if Object.const_defined?(:FOOBAR) | |
end | |
describe ".defined?" do | |
context "when the constant is defined" do | |
it "returns true" do | |
expect(described_class.defined?(:RUBY_REVISION)).to be true | |
end | |
end | |
context "when the constant is not defined" do | |
it "returns false" do | |
expect(described_class.defined?(:FOOBAR)).to be false | |
end | |
end | |
end | |
describe ".ensure" do | |
let(:old_value) { SecureRandom.uuid } | |
let(:new_value) { SecureRandom.uuid } | |
before do | |
allow(Object).to receive(:remove_const).with("FOOBAR").and_call_original | |
end | |
context "when the constant is not defined" do | |
it "assigns the value" do | |
described_class.ensure(:FOOBAR, new_value) | |
expect(FOOBAR).to eq new_value | |
end | |
end | |
context "when the constant was already defined" do | |
context "with the desired value" do | |
before do | |
Object.const_set(:FOOBAR, new_value) | |
end | |
it "assigns the value" do | |
expect(Object).to_not receive(:remove_const).with("FOOBAR") | |
expect { described_class.ensure(:FOOBAR, new_value) }.to_not change { FOOBAR } | |
end | |
end | |
context "with a different value" do | |
before do | |
Object.const_set(:FOOBAR, old_value) | |
end | |
it "assigns the value" do | |
expect { described_class.ensure(:FOOBAR, new_value) }.to change { FOOBAR }.from(old_value).to(new_value) | |
end | |
end | |
end | |
end | |
describe ".get" do | |
context "when the constant is defined" do | |
it "returns true" do | |
expect(described_class.get(:RUBY_REVISION)).to eq RUBY_REVISION | |
end | |
end | |
context "when the constant is not defined" do | |
it "returns false" do | |
expect { described_class.get(:FOOBAR) }.to raise_error(NameError) | |
end | |
end | |
end | |
describe ".redefine" do | |
let(:old_value) { SecureRandom.uuid } | |
let(:new_value) { SecureRandom.uuid } | |
context "when the constant is not defined" do | |
it "assigns the value" do | |
described_class.redefine(:FOOBAR, new_value) | |
expect(FOOBAR).to eq new_value | |
end | |
end | |
context "when the constant was already defined" do | |
before do | |
Object.const_set(:FOOBAR, old_value) | |
end | |
it "assigns the value" do | |
expect { described_class.redefine(:FOOBAR, new_value) }.to change { FOOBAR }.from(old_value).to(new_value) | |
end | |
end | |
end | |
describe ".set" do | |
let(:old_value) { SecureRandom.uuid } | |
let(:new_value) { SecureRandom.uuid } | |
context "when the constant is not defined" do | |
it "assigns the value" do | |
described_class.set(:FOOBAR, new_value) | |
expect(FOOBAR).to eq new_value | |
end | |
end | |
context "when the constant was already defined" do | |
before do | |
Object.const_set(:FOOBAR, old_value) | |
end | |
it "assigns the value" do | |
expect { described_class.set(:FOOBAR, new_value) }.to raise_error(NameError) | |
end | |
end | |
end | |
describe ".split" do | |
it "returns the namespace (if present) and basename of the constant" do | |
expect(described_class.split("::SecureRandom::FOOBAR")).to eq ["::SecureRandom", "FOOBAR"] | |
expect(described_class.split("SecureRandom::FOOBAR")).to eq ["SecureRandom", "FOOBAR"] | |
expect(described_class.split("::FOOBAR")).to eq [nil, "FOOBAR"] | |
expect(described_class.split("FOOBAR")).to eq [nil, "FOOBAR"] | |
end | |
end | |
describe ".unpack" do | |
it "returns the namespace (if present) and basename of the constant" do | |
expect(described_class.unpack("::SecureRandom::FOOBAR")).to eq [SecureRandom, "FOOBAR"] | |
expect(described_class.unpack("SecureRandom::FOOBAR")).to eq [SecureRandom, "FOOBAR"] | |
expect(described_class.unpack("::FOOBAR")).to eq [Object, "FOOBAR"] | |
expect(described_class.unpack("FOOBAR")).to eq [Object, "FOOBAR"] | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment