478 lines
12 KiB
Ruby
478 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "pathname"
|
|
|
|
RSpec.describe Dry::Configurable, ".setting" do
|
|
shared_context "configurable behavior" do
|
|
context "without a default value" do
|
|
before do
|
|
klass.setting :db
|
|
end
|
|
|
|
it "sets nil as the default" do
|
|
expect(object.config.db).to be(nil)
|
|
end
|
|
|
|
it "allows configuring a setting using a writer" do
|
|
object.config.db = "sqlite"
|
|
|
|
expect(object.config.db).to eql("sqlite")
|
|
end
|
|
|
|
it "allows configuring a setting using a square-bracket writer" do
|
|
object.config[:db] = "sqlite"
|
|
|
|
expect(object.config.db).to eql("sqlite")
|
|
|
|
object.config[:db] = "mariadb"
|
|
|
|
expect(object.config.db).to eql("mariadb")
|
|
end
|
|
end
|
|
|
|
it "raises when invalid options are passed" do
|
|
expect {
|
|
klass.setting :db_config, user: "root", password: "", reader: true
|
|
}.to raise_error(
|
|
ArgumentError, "Invalid options: [:user, :password]"
|
|
)
|
|
end
|
|
|
|
it "stores setting name as symbol" do
|
|
klass.setting "db", default: "sqlite"
|
|
|
|
expect(object.config.values.keys).to include(:db)
|
|
end
|
|
|
|
context "with a default value" do
|
|
context "string" do
|
|
before do
|
|
klass.setting :db, default: "sqlite"
|
|
end
|
|
|
|
it "presets the default value" do
|
|
expect(object.config.db).to eql("sqlite")
|
|
end
|
|
end
|
|
|
|
context "hash" do
|
|
it "returns the default value" do
|
|
klass.setting :db_config, default: {user: "root", password: ""}
|
|
|
|
expect(object.config.db_config).to eql(user: "root", password: "")
|
|
end
|
|
|
|
it "copies the original hash object" do
|
|
hash = {user: "root", password: ""}
|
|
|
|
klass.setting :db_config, default: hash
|
|
|
|
expect(object.config.db_config).to_not be(hash)
|
|
expect(object.config.db_config).to eql(hash)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with nested settings" do
|
|
before do
|
|
klass.setting :db do
|
|
setting :type, default: "sqlite"
|
|
setting :cred do
|
|
setting :user
|
|
setting :pass
|
|
end
|
|
end
|
|
end
|
|
|
|
it "nests values in the config" do
|
|
expect(object.config.db.type).to eql("sqlite")
|
|
expect(object.config.db.cred.user).to be(nil)
|
|
expect(object.config.db.cred.pass).to be(nil)
|
|
|
|
object.config.db.cred.user = "root"
|
|
object.config.db.cred.pass = "secret"
|
|
|
|
expect(object.config.db.cred.user).to eql("root")
|
|
expect(object.config.db.cred.pass).to eql("secret")
|
|
end
|
|
end
|
|
|
|
context "with a value constructor" do
|
|
it "constructs the value with nil default" do
|
|
klass.setting(:path, default: nil, constructor: ->(value) { "test:#{value || "fallback"}" })
|
|
|
|
expect(object.config.path).to eql("test:fallback")
|
|
|
|
object.configure do |config|
|
|
config.path = "foo"
|
|
end
|
|
|
|
expect(object.config.path).to eql("test:foo")
|
|
end
|
|
|
|
it "constructs the value with undefined default" do
|
|
klass.setting(:path, constructor: ->(value) { "test:#{value || "fallback"}" })
|
|
|
|
expect(object.config.path).to eql("test:fallback")
|
|
end
|
|
|
|
it "constructs the value with non-nil default" do
|
|
klass.setting(:path, default: "test", constructor: ->(value) { Pathname(value) })
|
|
|
|
expect(object.config.path).to eql(Pathname("test"))
|
|
end
|
|
|
|
it "raises constructor errors immediately" do
|
|
klass.setting(:failable, constructor: ->(value) { value&.to_sym })
|
|
|
|
expect {
|
|
object.config.failable = 12
|
|
}.to raise_error(NoMethodError, /undefined method `to_sym'/)
|
|
end
|
|
end
|
|
|
|
context "with reader: true" do
|
|
it "defines a reader shortcut when there is no default" do
|
|
klass.setting :db, reader: true
|
|
|
|
expect(object.db).to be(nil)
|
|
end
|
|
|
|
it "defines a reader shortcut when there is default" do
|
|
klass.setting :db, default: "sqlite", reader: true
|
|
|
|
expect(object.db).to eql("sqlite")
|
|
end
|
|
end
|
|
|
|
context "with a ruby keyword" do
|
|
before do
|
|
klass.setting :if, default: true
|
|
end
|
|
|
|
it "works" do
|
|
expect(object.config.if).to be(true)
|
|
end
|
|
end
|
|
|
|
context "with :settings as a setting name" do
|
|
before do
|
|
klass.setting :settings, default: true
|
|
end
|
|
|
|
it "works" do
|
|
expect(object.config.settings).to be(true)
|
|
end
|
|
end
|
|
|
|
it "rejects invalid names" do
|
|
%i[foo? bar! d'oh 7 {} - ==].each do |name|
|
|
expect { klass.setting name }.to raise_error(ArgumentError, /not a valid/)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when extended" do
|
|
subject(:object) do
|
|
klass
|
|
end
|
|
|
|
let(:klass) do
|
|
Class.new do
|
|
extend Dry::Configurable
|
|
end
|
|
end
|
|
|
|
include_context "configurable behavior"
|
|
|
|
specify "settings defined after accessing config are still available in the config" do
|
|
klass.setting :before, default: "defined before"
|
|
klass.config
|
|
klass.setting :after, default: "defined after"
|
|
|
|
expect(klass.config.before).to eq "defined before"
|
|
expect(klass.config.after).to eq "defined after"
|
|
end
|
|
|
|
context "with a subclass" do
|
|
let(:subclass) do
|
|
Class.new(klass)
|
|
end
|
|
|
|
it "maintains mutated value in a child config" do
|
|
klass.setting :db do
|
|
setting :ports, default: Set[123]
|
|
end
|
|
|
|
klass.config.db.ports << 312
|
|
|
|
subclass = Class.new(klass)
|
|
|
|
expect(subclass.config.db.ports).to eql(Set[123, 312])
|
|
end
|
|
|
|
it "allows defining more settings" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
subclass.setting :username, default: "root"
|
|
subclass.setting :password
|
|
|
|
subclass.config.password = "secret"
|
|
|
|
expect(subclass.config.db).to eql("sqlite")
|
|
expect(subclass.config.username).to eql("root")
|
|
expect(subclass.config.password).to eql("secret")
|
|
end
|
|
|
|
it "adding parent setting does not affect child" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
expect(subclass.settings).to eql(Set[:db])
|
|
|
|
klass.setting :other
|
|
|
|
expect(subclass.settings).to eql(Set[:db])
|
|
end
|
|
|
|
specify "configuring the parent before subclassing copies the config to the child" do
|
|
klass.setting :db
|
|
|
|
object.config.db = "mariadb"
|
|
|
|
expect(subclass.config.db).to eql("mariadb")
|
|
end
|
|
|
|
specify "configuring the parent after subclassing does not copy the config to the child" do
|
|
klass.setting :db
|
|
|
|
subclass = Class.new(klass)
|
|
|
|
object.config.db = "mariadb"
|
|
|
|
expect(subclass.config.db).to be nil
|
|
end
|
|
|
|
it "not configured parent does not set child config" do
|
|
klass.setting :db
|
|
|
|
expect(subclass.config.db).to be(nil)
|
|
end
|
|
|
|
it "changing child does not affect parent" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
klass.setting :nested do
|
|
setting :test, default: "hello"
|
|
end
|
|
|
|
subclass.configure do |config|
|
|
config.db = "postgresql"
|
|
config.nested.test = "woah!"
|
|
end
|
|
|
|
expect(klass.settings).to eql(Set[:db, :nested])
|
|
expect(object.config.db).to eql("sqlite")
|
|
expect(object.config.db).to eql("sqlite")
|
|
expect(object.config.nested.test).to eql("hello")
|
|
|
|
expect(subclass.settings).to eql(Set[:db, :nested])
|
|
expect(subclass.config.db).to eql("postgresql")
|
|
expect(subclass.config.nested.test).to eql("woah!")
|
|
end
|
|
|
|
it "inherits readers from parent" do
|
|
klass.setting :db, default: "sqlite", reader: true
|
|
|
|
expect(subclass.db).to eql("sqlite")
|
|
end
|
|
|
|
it "defines a reader shortcut for nested config" do
|
|
klass.setting :dsn, reader: true do
|
|
setting :pool, default: 5
|
|
end
|
|
|
|
expect(klass.dsn.pool).to be(5)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when included" do
|
|
subject(:object) do
|
|
klass.new
|
|
end
|
|
|
|
let(:klass) do
|
|
Class.new do
|
|
include Dry::Configurable
|
|
end
|
|
end
|
|
|
|
include_context "configurable behavior"
|
|
|
|
it "creates config detached from the class settings" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
object.config.db = "mariadb"
|
|
|
|
expect(object.config.db).to eql("mariadb")
|
|
expect(klass.new.config.db).to eql("sqlite")
|
|
end
|
|
|
|
it "exposes `config` only at the instance-level" do
|
|
expect(klass).to_not respond_to(:config)
|
|
end
|
|
|
|
it "exposes `configure` only at the instance-level" do
|
|
expect(klass).to_not respond_to(:configure)
|
|
end
|
|
|
|
it "defines a constructor that sets the config" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
expect(object.config.db).to eql("sqlite")
|
|
end
|
|
|
|
it "creates distinct setting values across instances" do
|
|
klass.setting(:path, default: "test", constructor: ->(m) { Pathname(m) })
|
|
|
|
new_object = klass.new
|
|
|
|
expect(object.config.path).to eq Pathname("test")
|
|
expect(new_object.config.path).to eq Pathname("test")
|
|
expect(object.config.path).not_to be(new_object.config.path)
|
|
end
|
|
|
|
it "makes only settings defined before instantiation available" do
|
|
klass.setting :before, default: "defined before"
|
|
|
|
object_1 = klass.new
|
|
|
|
klass.setting :after, default: "defined after"
|
|
|
|
object_2 = klass.new
|
|
|
|
expect(object_1.config.before).to eq "defined before"
|
|
expect(object_1.config).not_to respond_to(:after)
|
|
|
|
expect(object_2.config.before).to eq "defined before"
|
|
expect(object_2.config.after).to eq "defined after"
|
|
end
|
|
|
|
shared_examples "copying" do
|
|
before do
|
|
klass.setting :env
|
|
|
|
klass.setting :db do
|
|
setting :user, default: "root".dup
|
|
setting :pass, default: "secret"
|
|
end
|
|
end
|
|
|
|
it "can be copied" do
|
|
clone = object.clone
|
|
|
|
expect(object.config.env).to be(nil)
|
|
expect(clone.config.env).to be(nil)
|
|
|
|
expect(object.config.db.user).to eql("root")
|
|
expect(clone.config.db.user).to eql("root")
|
|
|
|
object.config.env = "production"
|
|
object.config.db.user = "jane"
|
|
|
|
expect(object.config.env).to eql("production")
|
|
|
|
expect(object.config.db.user).to eql("jane")
|
|
expect(clone.config.db.user).to eql("root")
|
|
|
|
expect(clone.config.db.pass).to eql(object.config.db.pass)
|
|
end
|
|
end
|
|
|
|
include_examples "copying" do
|
|
it "stays frozen when cloning" do
|
|
expect(object.finalize!.clone).to be_frozen
|
|
end
|
|
|
|
it "stays unfrozen when duping" do
|
|
expect(object.finalize!.dup).to_not be_frozen
|
|
end
|
|
end
|
|
|
|
it "can be configured" do
|
|
klass.setting :db, default: "sqlite"
|
|
|
|
object.configure do |config|
|
|
config.db = "mariadb"
|
|
end
|
|
|
|
expect(object.config.db).to eql("mariadb")
|
|
end
|
|
|
|
it "can be finalized" do
|
|
klass.setting :kafka, default: "kafka://127.0.0.1:9092"
|
|
|
|
object.finalize!
|
|
# becomes a no-op
|
|
object.finalize!
|
|
|
|
expect(object).to be_frozen
|
|
expect(object.config.db).to be_frozen
|
|
expect(object.config.db.user).not_to be_frozen
|
|
|
|
object.config.db.user << "foo"
|
|
expect(object.config.db.user).to eq("rootfoo")
|
|
|
|
# does not allow configure block anymore
|
|
expect { object.configure {} }.to raise_error(Dry::Configurable::FrozenConfig)
|
|
end
|
|
|
|
it "can be finalized with freezing values" do
|
|
klass.setting :kafka, "kafka://127.0.0.1:9092"
|
|
|
|
object.finalize!(freeze_values: true)
|
|
# becomes a no-op
|
|
object.finalize!(freeze_values: true)
|
|
|
|
expect(object).to be_frozen
|
|
expect(object.config.db).to be_frozen
|
|
expect(object.config.db.user).to be_frozen
|
|
|
|
expect { object.config.db.user << "foo" }.to raise_error(FrozenError)
|
|
|
|
# does not allow configure block anymore
|
|
expect { object.configure {} }.to raise_error(Dry::Configurable::FrozenConfig)
|
|
end
|
|
|
|
it "defines a reader shortcut for nested config" do
|
|
klass.setting :dsn, reader: true do
|
|
setting :pool, default: 5
|
|
end
|
|
|
|
expect(object.dsn.pool).to be(5)
|
|
end
|
|
|
|
context "Test Interface" do
|
|
describe "reset_config" do
|
|
it "resets configuration to default values" do
|
|
klass.setting :dsn, default: nil
|
|
|
|
klass.setting :pool do
|
|
setting :size, default: nil
|
|
end
|
|
|
|
object.enable_test_interface
|
|
|
|
object.config.dsn = "sqlite:memory"
|
|
object.config.pool.size = 5
|
|
|
|
object.reset_config
|
|
|
|
expect(object.config.dsn).to be_nil
|
|
expect(object.config.pool.size).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|