diff --git a/Gemfile b/Gemfile index 0ccff33a..2ef950b4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,16 +1,16 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" -eval_gemfile 'Gemfile.devtools' +eval_gemfile "Gemfile.devtools" gemspec group :benchmarks do - gem 'benchmark-ips' + gem "benchmark-ips" end group :tools do - gem 'hotch' - gem 'pry-byebug', platform: :mri + gem "hotch" + gem "pry-byebug", platform: :mri end diff --git a/Rakefile b/Rakefile index ad52f833..be26d08b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'rspec/core/rake_task' +require "rspec/core/rake_task" -desc 'Run all specs in spec directory' +desc "Run all specs in spec directory" RSpec::Core::RakeTask.new(:spec) task default: :spec diff --git a/bin/console b/bin/console index 936f50ef..0afe5714 100755 --- a/bin/console +++ b/bin/console @@ -1,13 +1,13 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'bundler/setup' -require 'dry/configurable' +require "bundler/setup" +require "dry/configurable" begin - require 'pry-byebug' + require "pry-byebug" Pry.start rescue LoadError - require 'irb' + require "irb" IRB.start end diff --git a/lib/dry/configurable/setting.rb b/lib/dry/configurable/setting.rb index dd36f023..e59268c8 100644 --- a/lib/dry/configurable/setting.rb +++ b/lib/dry/configurable/setting.rb @@ -42,6 +42,11 @@ class Setting class Nested < Setting CONSTRUCTOR = Config.method(:new) + def merge(setting) + settings = input.merge(setting.input) + Nested.new(name, input: settings, **setting.options) + end + # @api private def pristine with(input: input.pristine) diff --git a/lib/dry/configurable/settings.rb b/lib/dry/configurable/settings.rb index 3784e17d..899809ec 100644 --- a/lib/dry/configurable/settings.rb +++ b/lib/dry/configurable/settings.rb @@ -23,6 +23,28 @@ def initialize(elements = EMPTY_ARRAY) initialize_elements(elements) end + def replace(settings) + ensure_arguments(settings) + + settings.dup.each do |setting| + self << setting + end + end + + def merge!(settings) + merge(settings).each do |setting| + self << setting + end + end + + def merge(settings) + ensure_arguments(settings) + + settings.dup.inject(dup) do |memo, setting| + memo << merge_setting(setting) + end + end + # @api private def <<(setting) elements[setting.name] = setting @@ -56,6 +78,20 @@ def pristine private + def merge_setting(setting) + if elements[setting.name].is_a?(Setting::Nested) && setting.is_a?(Setting::Nested) + elements[setting.name].merge(setting) + else + setting + end + end + + def ensure_arguments(settings) + unless settings.is_a? Dry::Configurable::Settings + raise ArgumentError, "settings must be a Dry::Configurable::Settings" + end + end + # @api private def initialize_copy(source) initialize_elements(source.map(&:dup)) diff --git a/spec/integration/dry/configurable/settings_spec.rb b/spec/integration/dry/configurable/settings_spec.rb new file mode 100644 index 00000000..104595a2 --- /dev/null +++ b/spec/integration/dry/configurable/settings_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require "pathname" + +RSpec.describe Dry::Configurable::Settings do + context "can be configured with another class's settings" do + let(:klass) do + Class.new do + extend Dry::Configurable + end + end + + context "with replace" do + let(:other_klass) do + Class.new do + extend Dry::Configurable + end + end + + it "should replace nested fields" do + klass.setting :database do + setting :host, "localhost" + end + + other_klass.setting :database do + setting :type, "postgresql" + end + other_klass._settings.replace(klass._settings) + expect(other_klass.config.database.host).to eql("localhost") + expect { other_klass.config.database.type }.to raise_error do |error| + expect(error.class).to eql(NoMethodError) + end + end + end + + context "with merge" do + let(:other_klass) do + Class.new do + extend Dry::Configurable + end + end + + it "should override fields and return a Dry::Configurable::Settings" do + klass.setting :database do + setting :type, "postgresql" + setting :host, "remote" + end + + other_klass.setting :database do + setting :host, "localhost" + setting :port, 54_321 + end + + settings = other_klass._settings.merge(klass._settings) + + aggregate_failures do + expect(settings.class).to be(Dry::Configurable::Settings) + expect(settings[:database].input.entries.size).to eql(3) + expect(settings[:database].input[:type].default).to eql("postgresql") + expect(settings[:database].input[:host].default).to eql("remote") + expect(settings[:database].input[:port].default).to eql(54_321) + end + + aggregate_failures do + expect(other_klass.config.database.host).to eql("localhost") + expect(other_klass.config.database.port).to eql(54_321) + expect { other_klass.config.database.type }.to raise_error do |error| + expect(error.class).to eql(NoMethodError) + end + end + + aggregate_failures do + expect(klass.config.database.host).to eql("remote") + expect(klass.config.database.type).to eql("postgresql") + expect { klass.config.database.port }.to raise_error do |error| + expect(error.class).to eql(NoMethodError) + end + end + end + end + + context "with merge!" do + let(:other_klass) do + Class.new do + extend Dry::Configurable + end + end + + it "should override a block" do + klass.setting(:hello, "bar") { |w| w.chars.join("_") } + other_klass.setting(:hello, "fuzz") { |w| w.chars.join("-") } + expect(other_klass.config.hello).to eql("f-u-z-z") + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.hello).to eql("b_a_r") + end + + it "replaces undefined fields" do + klass.setting :hello, "world" + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.hello).to eql("world") + end + + it "replaces deep fields" do + klass.setting :database do + setting :dsn, "localhost" + end + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.database.dsn).to eql("localhost") + end + + it "overrides deep fields" do + klass.setting :database do + setting :dsn, "localhost" + end + other_klass.setting :database do + setting :dsn, "remote" + end + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.database.dsn).to eql("localhost") + end + + it "shouldn't blow up when merging a non nested setting to a nested setting" do + klass.setting :database, "hello" + other_klass.setting :database do + setting :dsn, "localhost" + end + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.database).to eql("hello") + end + + it "should work when the config accessor has already be invoked" do + klass.setting :database do + setting :dsn, "localhost" + end + other_klass.setting :database do + setting :dsn, "remote" + end + expect(other_klass.config.database.dsn).to eql("remote") + other_klass._settings.merge!(klass._settings) + expect(other_klass.config.database.dsn).to eql("localhost") + end + + it "throws an error if the settings aren't Dry::Configurable::Settings" do + klass.setting :hello, "world" + expect { other_klass._settings.merge!(klass) }.to raise_error do |error| + expect(error.class).to be(ArgumentError) + end + end + end + end +end