spec/lib/gdk/config_settings_spec.rb (368 lines of code) (raw):

# frozen_string_literal: true RSpec.describe GDK::ConfigSettings do subject(:config) { described_class.new } describe '.array' do it 'accepts an array' do described_class.array(:foo) { %w[a b] } expect { config.foo }.not_to raise_error end it 'accepts a string' do described_class.array(:foo) { 'foo' } expect { config.foo }.not_to raise_error end it 'fails on non-array and non-string value' do described_class.array(:foo) { 123 } expect { config.foo }.to raise_error(TypeError) end context 'when there is YAML defined' do let(:test_klass) do new_test_klass do |s| s.array(:foo, merge: true) { %w[a] } end end subject(:config) { test_klass.new(yaml: { 'foo' => %w[b] }) } it 'is mergeable' do expect(config.foo).to eq(%w[b a]) end end end describe '.hash_setting' do it 'accepts a hash' do described_class.hash_setting(:foo) { { 'a' => 'A' } } expect { config.foo }.not_to raise_error end it 'accepts a JSON parseable string' do described_class.hash_setting(:foo) { { 'a' => '{}' } } expect { config.foo }.not_to raise_error end it 'fails on non-JSON-parseable non-array value' do described_class.hash_setting(:foo) { %q(a b) } expect { config.foo }.to raise_error(GDK::StandardErrorWithMessage) end context 'when there is YAML defined' do let(:test_klass) do new_test_klass do |s| s.hash_setting(:foo, merge: true) { { 'a' => { 'A' => 'A' } } } end end context 'added values' do subject(:config) { test_klass.new(yaml: { 'foo' => { 'b' => { 'B' => 'B' } } }) } it 'values are merged' do expect(config.foo).to eq({ 'a' => { 'A' => 'A' }, 'b' => { 'B' => 'B' } }) end end context 'overridden values' do subject(:config) { test_klass.new(yaml: { 'foo' => { 'a' => 'B' } }) } it 'value is overridden' do expect(config.foo).to eq({ 'a' => 'B' }) end end describe '#dump!' do context 'when not user defined' do subject(:config) { test_klass.new } it 'dumps depending on the flag' do expect(config.dump!).to eq({ 'foo' => { 'a' => { 'A' => 'A' } } }) expect(config.dump!(user_only: true)).to eq({}) end end context 'with user defined values' do subject(:config) { test_klass.new(yaml: { 'foo' => { 'a' => 'B' } }) } it 'dumps depending on the flag' do expect(config.dump!).to eq({ 'foo' => { 'a' => 'B' } }) expect(config.dump!(user_only: true)).to eq({ 'foo' => { 'a' => 'B' } }) end end end end end describe '.bool' do it 'accepts a bool' do described_class.bool(:foo) { 'false' } expect { config.foo }.not_to raise_error expect(config.foo).to be(false) end it 'accepts a bool?' do described_class.bool(:foo) { 'false' } expect { config.foo? }.not_to raise_error expect(config.foo?).to be(false) end it 'fails on non-bool value' do described_class.bool(:foo) { 'hello' } expect { config.foo }.to raise_error(TypeError) end end describe '.integer' do it 'accepts an integer' do described_class.integer(:foo) { '333' } expect { config.foo }.not_to raise_error expect(config.foo).to eq(333) end it 'fails on non-integer value' do described_class.integer(:foo) { '33d' } expect { config.foo }.to raise_error(TypeError) end end describe '.path' do it 'accepts a valid path' do described_class.path(:foo) { '/tmp' } expect { config.foo }.not_to raise_error expect(config.foo).to be_a(Pathname) expect(config.foo.to_s).to eq('/tmp') end it 'fails on non-path' do described_class.path(:foo) { nil } expect { config.foo }.to raise_error(TypeError) end end describe '.string' do it 'accepts a string' do described_class.string(:foo) { 'howdy' } expect { config.foo }.not_to raise_error expect(config.foo).to eq('howdy') end it 'fails on non-string' do described_class.string(:foo) { nil } expect { config.foo }.to raise_error(TypeError) end end describe 'dynamic setting' do let(:test_klass) do new_test_klass do |s| s.string(:bar) { 'hello' } end end subject(:config) { test_klass.load_from_file } it 'can read a setting' do expect(config.bar).to eq('hello') end context 'with foo.yml' do before do File.write(test_klass::FILE, { 'bar' => 'baz' }.to_yaml) end after do File.unlink(test_klass::FILE) end it 'reads settings from yaml' do expect(config.bar).to eq('baz') end end end describe '.settings_array' do it 'creates an array of the desired size' do described_class.settings_array(:foo, size: 3) { nil } expect(config.foo.count).to eq(3) end it 'creates an array of the desired size using block' do described_class.integer(:baz) { 'four'.length } described_class.settings_array(:foo, size: -> { baz }) { nil } expect(config.foo.count).to eq(4) end it 'creates array with self as parent' do described_class.settings_array(:foo, size: 1) { nil } expect(config.foo.parent).to eq(config) end it 'creates array of settings with self as grandparent' do described_class.settings_array(:foo, size: 1) { nil } expect(config.foo.first.parent.parent).to eq(config) end it 'attributes are available through root config' do config = Class.new(GDK::ConfigSettings) do settings_array(:arrrr, size: 3) do |i| string(:buz) { "sub #{i}" } end end.new expect(config.arrrr.map(&:buz)).to eq(['sub 0', 'sub 1', 'sub 2']) end context 'indexed access' do let(:config) { test_class.new } let(:test_class) do new_test_klass do |s| s.settings :wrapped do settings_array(:fruits, size: 3) do |i| string(:name) { "apple #{i}" } end end end end it 'retrieves values via dig' do expect(config.dig('wrapped', 'fruits', 0, 'name')).to eq('apple 0') end it 'sets values via bury!' do config.bury!('wrapped.fruits.0.name', 'banana') expect(config.dig('wrapped', 'fruits', 0, 'name')).to eq('banana') end it 'checks user defined values' do expect(config.user_defined?('wrapped', 'fruits', 0)).to be false config.bury!('wrapped.fruits.0.name', 'banana') expect(config.user_defined?('wrapped', 'fruits', 0)).to be true expect(config.user_defined?('wrapped', 'fruits', 0, 'name')).to be true end it 'defines __index setting automatically' do expect(config.wrapped.fruits.map(&:__index)).to eq([0, 1, 2]) end end context 'with dynamic size' do let(:yaml) { {} } let(:config) do Class.new(GDK::ConfigSettings) do settings_array(:items) do |i| string(:name) { "name #{i}" } end end.new(yaml: yaml) end it 'is empty' do expect(config.items.map(&:name)).to eq([]) end context 'when configured in yaml' do let(:yaml) do { "items" => [ { "name" => "test" }, {} ] } end it 'has the configured size' do expect(config.items.map(&:name)).to eq(["test", "name 1"]) end end end end describe '#validate!' do context 'when valid' do it 'returns nil' do described_class.integer(:foo) { '333' } described_class.string(:bar) { 'howdy' } expect(config.validate!).to be_nil end end context 'when invalid' do it 'raises exception' do described_class.integer(:foo) { 'a funny string' } described_class.string(:bar) { 'howdy' } expect { config.validate! }.to raise_error(TypeError) end end context 'with a nested port conflict' do let(:test_klass) do new_test_klass do |s| s.settings(:service_a) do port(:port, 'a') { 3333 } end s.settings(:service_b) do port(:port, 'b') { 3333 } end end end it 'raises an error' do expect { test_klass.new.validate! }.to raise_error("Value '3333' for setting 'service_b.port' is not a valid port - Port 3333 is already allocated for service 'a'.") end end end describe '#dump!' do it 'generates configs without ignored ones' do described_class.integer(:foo) { '333' } described_class.string(:bar) { 'howdy' } described_class.settings_array(:baz, size: 2) { string(:name) { 'bonza' } } described_class.integer(:__internal_foo) { '333' } described_class.integer(:questionable_foo?) { '333' } expect(config.dump!).to eq( 'foo' => 333, 'bar' => 'howdy', 'baz' => [{ 'name' => 'bonza' }, { 'name' => 'bonza' }] ) end context 'when includes user_only configs' do let(:yaml) { { 'bar' => 'whassup dude', 'baz' => [{}, { 'name' => 'fonzi' }] } } let(:config) { described_class.new(yaml: yaml) } it 'generates only user_only configs' do described_class.integer(:foo) { '333' } described_class.string(:bar) { 'howdy' } described_class.settings_array(:baz, size: 3) { string(:name) { 'bonza' } } expect(config.dump!(user_only: true)).to eq( 'bar' => 'whassup dude', 'baz' => [{}, { 'name' => 'fonzi' }] ) end end end describe '#dump_as_yaml' do it 'generates configs' do described_class.integer(:foo) { '333' } described_class.string(:bar) { 'howdy' } described_class.settings_array(:baz, size: 1) { string(:name) { 'bonza' } } expect(config.dump_as_yaml).to eq(<<~YAML) --- bar: howdy baz: - name: bonza foo: 333 YAML end end describe '#bury!' do it 'assigns value in the yaml' do key = 'foo' described_class.integer(key) { '333' } expect { config.bury!(key, '444') }.to change(config, key).to(444) end it 'raises an error when burying a port to a boolean' do key = 'foo' described_class.integer(key) { '333' } current_port = config[key] expect { config.bury!(key, false) }.to raise_error(TypeError, "Value 'false' for setting '#{key}' is not a valid integer.") expect(config[key]).to eq(current_port) end it 'buries into non-existing subsettings' do described_class.settings(:foo) { string(:name) { 'bonza' } } expect { config.bury!('foo.name', 'ripper') } .to change { config.yaml }.to('foo' => { 'name' => 'ripper' }) end it 'buries next to existing subsettings' do described_class.settings(:foo) { string(:name) { 'bonza' } } config = described_class.new(yaml: { 'foo' => { 'location' => 'down under' } }) expect { config.bury!('foo.name', 'ripper') } .to change { config.yaml }.to('foo' => { 'name' => 'ripper', 'location' => 'down under' }) end end describe '#save_yaml!' do context 'with foo.yml' do before do stub_backup end after do File.unlink(test_klass::FILE) end let(:test_klass) do new_test_klass do |s| s.integer(:port) { 3000 } end end subject(:config) { test_klass.load_from_file } it 'saves to file' do expect(File).to receive(:write).and_call_original config.save_yaml! end it 'returns nil' do expect(config.save_yaml!).to be_nil end it 'saves config twice without errors' do config.save_yaml! expect { config.save_yaml! }.not_to raise_error end context 'when YAML was modified out of band' do let(:other_config) { test_klass.load_from_file } before do config # load the config other_config.save_yaml! end it 'raises an error' do expect { config.save_yaml! }.to raise_error(described_class::YamlModified) end end end end def new_test_klass Class.new(described_class) do yield(self) const_set(:FILE, 'tmp/foo.yml') end end end