Skip to content

Instantly share code, notes, and snippets.

@leoarnold
Created March 11, 2021 09:27
Show Gist options
  • Save leoarnold/00b4fd3dbabadff50324400376fcf9ce to your computer and use it in GitHub Desktop.
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)
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
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