Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add convenience method Dry::Configurable::Settings#replace #117

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
10 changes: 5 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -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
8 changes: 4 additions & 4 deletions bin/console
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/dry/configurable/setting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions lib/dry/configurable/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down
151 changes: 151 additions & 0 deletions spec/integration/dry/configurable/settings_spec.rb
Original file line number Diff line number Diff line change
@@ -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