706 lines
20 KiB
Ruby
706 lines
20 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
RSpec.shared_examples "a container" do
|
|
describe "configuration" do
|
|
describe "registry" do
|
|
describe "default" do
|
|
it { expect(klass.config.registry).to be_a(Dry::Container::Registry) }
|
|
end
|
|
|
|
describe "custom" do
|
|
let(:custom_registry) { double("Registry") }
|
|
let(:key) { :key }
|
|
let(:item) { :item }
|
|
let(:options) { {} }
|
|
|
|
before do
|
|
klass.configure do |config|
|
|
config.registry = custom_registry
|
|
end
|
|
|
|
allow(custom_registry).to receive(:call)
|
|
end
|
|
|
|
after do
|
|
# HACK: Have to reset the configuration so that it doesn't
|
|
# interfere with other specs
|
|
klass.configure do |config|
|
|
config.registry = Dry::Container::Registry.new
|
|
end
|
|
end
|
|
|
|
subject! { container.register(key, item, options) }
|
|
|
|
it do
|
|
expect(custom_registry).to have_received(:call).with(
|
|
container._container,
|
|
key,
|
|
item,
|
|
options
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "resolver" do
|
|
describe "default" do
|
|
it { expect(klass.config.resolver).to be_a(Dry::Container::Resolver) }
|
|
end
|
|
|
|
describe "custom" do
|
|
let(:custom_resolver) { double("Resolver") }
|
|
let(:item) { double("Item") }
|
|
let(:key) { :key }
|
|
|
|
before do
|
|
klass.configure do |config|
|
|
config.resolver = custom_resolver
|
|
end
|
|
|
|
allow(custom_resolver).to receive(:call).and_return(item)
|
|
end
|
|
|
|
after do
|
|
# HACK: Have to reset the configuration so that it doesn't
|
|
# interfere with other specs
|
|
klass.configure do |config|
|
|
config.resolver = Dry::Container::Resolver.new
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve(key) }
|
|
|
|
it { expect(custom_resolver).to have_received(:call).with(container._container, key) }
|
|
it { is_expected.to eq(item) }
|
|
end
|
|
end
|
|
|
|
describe "namespace_separator" do
|
|
describe "default" do
|
|
it { expect(klass.config.namespace_separator).to eq(".") }
|
|
end
|
|
|
|
describe "custom" do
|
|
let(:custom_registry) { double("Registry") }
|
|
let(:key) { "key" }
|
|
let(:namespace_separator) { "-" }
|
|
let(:namespace) { "one" }
|
|
|
|
before do
|
|
klass.configure do |config|
|
|
config.namespace_separator = namespace_separator
|
|
end
|
|
|
|
container.namespace(namespace) do
|
|
register("key", "item")
|
|
end
|
|
end
|
|
|
|
after do
|
|
# HACK: Have to reset the configuration so that it doesn't
|
|
# interfere with other specs
|
|
klass.configure do |config|
|
|
config.namespace_separator = "."
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve([namespace, key].join(namespace_separator)) }
|
|
|
|
it { is_expected.to eq("item") }
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with default configuration" do
|
|
describe "registering a block" do
|
|
context "without options" do
|
|
context "without arguments" do
|
|
it "registers and resolves an object" do
|
|
container.register(:item) { "item" }
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item)).to eq("item")
|
|
end
|
|
end
|
|
|
|
context "with arguments" do
|
|
it "registers and resolves a proc" do
|
|
container.register(:item) { |item| item }
|
|
|
|
expect(container.resolve(:item).call("item")).to eq("item")
|
|
end
|
|
|
|
it "does not call a proc on resolving if one accepts an arbitrary number of keyword arguments" do
|
|
container.register(:item) { |*| "item" }
|
|
|
|
expect(container.resolve(:item)).to be_a_kind_of Proc
|
|
expect(container.resolve(:item).call).to eq("item")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with option call: false" do
|
|
it "registers and resolves a proc" do
|
|
container.register(:item, call: false) { "item" }
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item).call).to eq("item")
|
|
expect(container[:item].call).to eq("item")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "registering a proc" do
|
|
context "without options" do
|
|
context "without arguments" do
|
|
it "registers and resolves an object" do
|
|
container.register(:item, proc { "item" })
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item)).to eq("item")
|
|
expect(container[:item]).to eq("item")
|
|
end
|
|
end
|
|
|
|
context "with arguments" do
|
|
it "registers and resolves a proc" do
|
|
container.register(:item, proc { |item| item })
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item).call("item")).to eq("item")
|
|
expect(container[:item].call("item")).to eq("item")
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with option call: false" do
|
|
it "registers and resolves a proc" do
|
|
container.register(:item, proc { "item" }, call: false)
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item).call).to eq("item")
|
|
expect(container[:item].call).to eq("item")
|
|
end
|
|
end
|
|
|
|
context "with option memoize: true" do
|
|
it "registers and resolves a proc" do
|
|
container.register(:item, proc { "item" }, memoize: true)
|
|
|
|
expect(container[:item]).to be container[:item]
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item)).to eq("item")
|
|
expect(container[:item]).to eq("item")
|
|
end
|
|
|
|
it "only resolves the proc once" do
|
|
resolved_times = 0
|
|
|
|
container.register(:item, proc { resolved_times += 1 }, memoize: true)
|
|
|
|
expect(container.resolve(:item)).to be 1
|
|
expect(container.resolve(:item)).to be 1
|
|
end
|
|
|
|
context "when receiving something other than a proc" do
|
|
it do
|
|
expect { container.register(:item, "Hello!", memoize: true) }.to raise_error(Dry::Container::Error)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "registering an object" do
|
|
context "without options" do
|
|
it "registers and resolves the object" do
|
|
item = "item"
|
|
container.register(:item, item)
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item)).to be(item)
|
|
expect(container[:item]).to be(item)
|
|
end
|
|
end
|
|
|
|
context "with option call: false" do
|
|
it "registers and resolves an object" do
|
|
item = -> { "test" }
|
|
container.register(:item, item, call: false)
|
|
|
|
expect(container.keys).to eq(["item"])
|
|
expect(container.key?(:item)).to be true
|
|
expect(container.resolve(:item)).to eq(item)
|
|
expect(container[:item]).to eq(item)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "registering with the same key multiple times" do
|
|
it do
|
|
container.register(:item, proc { "item" })
|
|
|
|
expect { container.register(:item, proc { "item" }) }.to raise_error(Dry::Container::Error)
|
|
end
|
|
end
|
|
|
|
describe "resolving with a key that has not been registered" do
|
|
it do
|
|
expect(container.key?(:item)).to be false
|
|
expect { container.resolve(:item) }.to raise_error(KeyError) do |error|
|
|
# This is the API needed for DidYouMean::KeyErrorChecker to provide corrections
|
|
expect(error.key).to eq("item")
|
|
expect(error.receiver).to eq(container._container)
|
|
expect(error.spell_checker).to be_instance_of(DidYouMean::KeyErrorChecker)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "mixing Strings and Symbols" do
|
|
it do
|
|
container.register(:item, "item")
|
|
expect(container.resolve("item")).to eql("item")
|
|
end
|
|
end
|
|
|
|
describe "#merge" do
|
|
let(:key) { :key }
|
|
let(:other) { Dry::Container.new }
|
|
|
|
before do
|
|
other.register(key) { :item }
|
|
end
|
|
|
|
context "without namespace argument" do
|
|
subject! { container.merge(other) }
|
|
|
|
it { expect(container.resolve(key)).to be(:item) }
|
|
it { expect(container[key]).to be(:item) }
|
|
end
|
|
|
|
context "with namespace argument" do
|
|
subject! { container.merge(other, namespace: namespace) }
|
|
|
|
context "when namespace is nil" do
|
|
let(:namespace) { nil }
|
|
|
|
it { expect(container.resolve(key)).to be(:item) }
|
|
it { expect(container[key]).to be(:item) }
|
|
end
|
|
|
|
context "when namespace is not nil" do
|
|
let(:namespace) { "namespace" }
|
|
|
|
it { expect(container.resolve("#{namespace}.#{key}")).to be(:item) }
|
|
it { expect(container["#{namespace}.#{key}"]).to be(:item) }
|
|
end
|
|
end
|
|
|
|
context "with a block resolving conflicts" do
|
|
before do
|
|
container.register(:conflicting_key, "original")
|
|
other.register(:conflicting_key, "from other")
|
|
end
|
|
|
|
it "resolves conflict using provided block" do
|
|
container.merge(other) { |_, left, right| left }
|
|
|
|
expect(container[:conflicting_key]).to eql("original")
|
|
end
|
|
end
|
|
|
|
context "with a block resolving conflicts with a namespace" do
|
|
before do
|
|
container.register("items.conflicting_key", "original")
|
|
other.register("conflicting_key", "from other")
|
|
end
|
|
|
|
it "resolves conflict using provided block" do
|
|
container.merge(other, namespace: "items") { |_, left, right| left }
|
|
|
|
expect(container["items.conflicting_key"]).to eql("original")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#key?" do
|
|
let(:key) { :key }
|
|
|
|
before do
|
|
container.register(key) { :item }
|
|
end
|
|
|
|
subject! { container.key?(resolve_key) }
|
|
|
|
context "when key exists in container" do
|
|
let(:resolve_key) { key }
|
|
|
|
it { is_expected.to be true }
|
|
end
|
|
|
|
context "when key does not exist in container" do
|
|
let(:resolve_key) { :random }
|
|
|
|
it { is_expected.to be false }
|
|
end
|
|
end
|
|
|
|
describe "#keys" do
|
|
let(:keys) { [:key_1, :key_2] }
|
|
let(:expected_keys) { %w[key_1 key_2] }
|
|
|
|
before do
|
|
keys.each do |key|
|
|
container.register(key) { :item }
|
|
end
|
|
end
|
|
|
|
subject! { container.keys }
|
|
|
|
it "returns stringified versions of all registered keys" do
|
|
is_expected.to match_array(expected_keys)
|
|
end
|
|
end
|
|
|
|
describe "#each_key" do
|
|
let(:keys) { [:key_1, :key_2] }
|
|
let(:expected_keys) { %w[key_1 key_2] }
|
|
let!(:yielded_keys) { [] }
|
|
|
|
before do
|
|
keys.each do |key|
|
|
container.register(key) { :item }
|
|
end
|
|
end
|
|
|
|
subject! do
|
|
container.each_key { |key| yielded_keys << key }
|
|
end
|
|
|
|
it "yields stringified versions of all registered keys to the block" do
|
|
expect(yielded_keys).to match_array(expected_keys)
|
|
end
|
|
|
|
it "returns the container" do
|
|
is_expected.to eq(container)
|
|
end
|
|
end
|
|
|
|
describe "#each" do
|
|
let(:keys) { [:key_1, :key_2] }
|
|
let(:expected_key_value_pairs) { [%w[key_1 value_for_key_1], %w[key_2 value_for_key_2]] }
|
|
let!(:yielded_key_value_pairs) { [] }
|
|
|
|
before do
|
|
keys.each do |key|
|
|
container.register(key) { "value_for_#{key}" }
|
|
end
|
|
end
|
|
|
|
subject! do
|
|
container.each { |key, value| yielded_key_value_pairs << [key, value] }
|
|
end
|
|
|
|
it "yields stringified versions of all registered keys to the block" do
|
|
expect(yielded_key_value_pairs).to match_array(expected_key_value_pairs)
|
|
end
|
|
|
|
it "returns the container" do
|
|
is_expected.to eq(expected_key_value_pairs)
|
|
end
|
|
end
|
|
|
|
describe "#decorate" do
|
|
require "delegate"
|
|
|
|
let(:key) { :key }
|
|
let(:decorated_class_spy) { spy(:decorated_class_spy) }
|
|
let(:decorated_class) { Class.new }
|
|
|
|
context "for callable item" do
|
|
before do
|
|
allow(decorated_class_spy).to receive(:new) { decorated_class.new }
|
|
container.register(key, memoize: memoize) { decorated_class_spy.new }
|
|
container.decorate(key, with: SimpleDelegator)
|
|
end
|
|
|
|
context "memoize false" do
|
|
let(:memoize) { false }
|
|
|
|
it "does not call the block until the key is resolved" do
|
|
expect(decorated_class_spy).not_to have_received(:new)
|
|
container.resolve(key)
|
|
expect(decorated_class_spy).to have_received(:new)
|
|
end
|
|
|
|
specify do
|
|
expect(container[key]).to be_instance_of(SimpleDelegator)
|
|
expect(container[key].__getobj__).to be_instance_of(decorated_class)
|
|
expect(container[key]).not_to be(container[key])
|
|
expect(container[key].__getobj__).not_to be(container[key].__getobj__)
|
|
end
|
|
end
|
|
|
|
context "memoize true" do
|
|
let(:memoize) { true }
|
|
|
|
specify do
|
|
expect(container[key]).to be_instance_of(SimpleDelegator)
|
|
expect(container[key].__getobj__).to be_instance_of(decorated_class)
|
|
expect(container[key]).to be(container[key])
|
|
end
|
|
end
|
|
end
|
|
|
|
context "for not callable item" do
|
|
describe "wrapping" do
|
|
before do
|
|
container.register(key, call: false) { "value" }
|
|
container.decorate(key, with: SimpleDelegator)
|
|
end
|
|
|
|
it "expected to be an instance of SimpleDelegator" do
|
|
expect(container.resolve(key)).to be_instance_of(SimpleDelegator)
|
|
expect(container.resolve(key).__getobj__.call).to eql("value")
|
|
end
|
|
end
|
|
|
|
describe "memoization" do
|
|
before do
|
|
@called = 0
|
|
container.register(key, "value")
|
|
|
|
container.decorate(key) do |value|
|
|
@called += 1
|
|
"<#{value}>"
|
|
end
|
|
end
|
|
|
|
it "decorates static value only once" do
|
|
expect(container.resolve(key)).to eql("<value>")
|
|
expect(container.resolve(key)).to eql("<value>")
|
|
expect(@called).to be(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with an instance as a decorator" do
|
|
let(:decorator) do
|
|
double.tap do |decorator|
|
|
allow(decorator).to receive(:call) { |input| "decorated #{input}" }
|
|
end
|
|
end
|
|
|
|
before do
|
|
container.register(key) { "value" }
|
|
container.decorate(key, with: decorator)
|
|
end
|
|
|
|
it "expected to pass original value to decorator#call method" do
|
|
expect(container.resolve(key)).to eq("decorated value")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "namespace" do
|
|
context "when block does not take arguments" do
|
|
before do
|
|
container.namespace("one") do
|
|
register("two", 2)
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve("one.two") }
|
|
|
|
it "registers items under the given namespace" do
|
|
is_expected.to eq(2)
|
|
end
|
|
end
|
|
|
|
context "when block takes arguments" do
|
|
before do
|
|
container.namespace("one") do |c|
|
|
c.register("two", 2)
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve("one.two") }
|
|
|
|
it "registers items under the given namespace" do
|
|
is_expected.to eq(2)
|
|
end
|
|
end
|
|
|
|
context "with nesting" do
|
|
before do
|
|
container.namespace("one") do
|
|
namespace("two") do
|
|
register("three", 3)
|
|
end
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve("one.two.three") }
|
|
|
|
it "registers items under the given namespaces" do
|
|
is_expected.to eq(3)
|
|
end
|
|
end
|
|
|
|
context "with nesting and when block takes arguments" do
|
|
before do
|
|
container.namespace("one") do |c|
|
|
c.register("two", 2)
|
|
c.register("three", c.resolve("two"))
|
|
end
|
|
end
|
|
|
|
subject! { container.resolve("one.three") }
|
|
|
|
it "resolves items relative to the namespace" do
|
|
is_expected.to eq(2)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "import" do
|
|
it "allows importing of namespaces" do
|
|
ns = Dry::Container::Namespace.new("one") do
|
|
register("two", 2)
|
|
end
|
|
|
|
container.import(ns)
|
|
|
|
expect(container.resolve("one.two")).to eq(2)
|
|
end
|
|
|
|
it "allows importing of nested namespaces" do
|
|
ns = Dry::Container::Namespace.new("two") do
|
|
register("three", 3)
|
|
end
|
|
|
|
container.namespace("one") do
|
|
import(ns)
|
|
end
|
|
|
|
expect(container.resolve("one.two.three")).to eq(3)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "stubbing" do
|
|
before do
|
|
container.enable_stubs!
|
|
|
|
container.register(:item, "item")
|
|
container.register(:foo, "bar")
|
|
end
|
|
|
|
after do
|
|
container.unstub
|
|
end
|
|
|
|
it "keys can be stubbed" do
|
|
container.stub(:item, "stub")
|
|
expect(container.resolve(:item)).to eql("stub")
|
|
expect(container[:item]).to eql("stub")
|
|
end
|
|
|
|
it "only other keys remain accesible" do
|
|
container.stub(:item, "stub")
|
|
expect(container.resolve(:foo)).to eql("bar")
|
|
expect(container[:foo]).to eql("bar")
|
|
end
|
|
|
|
it "keys can be reverted back to their original value" do
|
|
container.stub(:item, "stub")
|
|
container.unstub(:item)
|
|
|
|
expect(container.resolve(:item)).to eql("item")
|
|
expect(container[:item]).to eql("item")
|
|
end
|
|
|
|
describe "with block argument" do
|
|
it "executes the block with the given stubs" do
|
|
expect { |b| container.stub(:item, "stub", &b) }.to yield_control
|
|
end
|
|
|
|
it "keys are stubbed only while inside the block" do
|
|
container.stub(:item, "stub") do
|
|
expect(container.resolve(:item)).to eql("stub")
|
|
end
|
|
|
|
expect(container.resolve(:item)).to eql("item")
|
|
end
|
|
end
|
|
|
|
describe "mixing Strings and Symbols" do
|
|
it do
|
|
container.stub(:item, "stub")
|
|
expect(container.resolve("item")).to eql("stub")
|
|
end
|
|
end
|
|
|
|
it "raises an error when key is missing" do
|
|
expect { container.stub(:non_existing, "something") }
|
|
.to raise_error(ArgumentError, 'cannot stub "non_existing" - no such key in container')
|
|
end
|
|
end
|
|
|
|
describe ".freeze" do
|
|
before do
|
|
container.register(:foo, "bar")
|
|
end
|
|
|
|
it "allows to freeze a container so that nothing can be registered later" do
|
|
container.freeze
|
|
expect { container.register(:baz, "quux") }.to raise_error(FrozenError)
|
|
expect(container).to be_frozen
|
|
end
|
|
|
|
it "wraps FrozenError to provide which key was attempted to be registered" do
|
|
container.freeze
|
|
expect { container.register(:baz, "quux") }
|
|
.to raise_error(
|
|
FrozenError,
|
|
/can't modify frozen \S+ \(when attempting to register 'baz'\)/
|
|
)
|
|
end
|
|
|
|
it "returns self back" do
|
|
expect(container.freeze).to be(container)
|
|
end
|
|
end
|
|
|
|
describe ".dup" do
|
|
it "returns a copy that doesn't share registered keys with the parent" do
|
|
container.dup.register(:foo, "bar")
|
|
expect(container.key?(:foo)).to be false
|
|
end
|
|
end
|
|
|
|
describe ".clone" do
|
|
it "returns a copy that doesn't share registered keys with the parent" do
|
|
container.clone.register(:foo, "bar")
|
|
expect(container.key?(:foo)).to be false
|
|
end
|
|
|
|
it "re-uses frozen container" do
|
|
expect(container.freeze.clone).to be_frozen
|
|
expect(container.clone._container).to be(container._container)
|
|
end
|
|
end
|
|
|
|
describe ".resolve" do
|
|
it "accepts a fallback block" do
|
|
expect(container.resolve("missing") { :fallback }).to be(:fallback)
|
|
end
|
|
end
|
|
end
|