diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index fdf9f0d4..10740ffb 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -143,10 +143,51 @@ def self.[](config) # :nodoc: unfinished API # If a block is given, the new config object is yielded to it. def initialize(parent = Config.global, **attrs) super(parent) - attrs.each do send(:"#{_1}=", _2) end + update(**attrs) yield self if block_given? end + # :call-seq: update(**attrs) -> self + # + # Assigns all of the provided +attrs+ to this config, and returns +self+. + # + # An ArgumentError is raised unless every key in +attrs+ matches an + # assignment method on Config. + # + # >>> + # *NOTE:* #update is not atomic. If an exception is raised due to an + # invalid attribute value, +attrs+ may be partially applied. + def update(**attrs) + unless (bad = attrs.keys.reject { respond_to?(:"#{_1}=") }).empty? + raise ArgumentError, "invalid config options: #{bad.join(", ")}" + end + attrs.each do send(:"#{_1}=", _2) end + self + end + + # :call-seq: + # with(**attrs) -> config + # with(**attrs) {|config| } -> result + # + # Without a block, returns a new config which inherits from self. With a + # block, yields the new config and returns the block's result. + # + # If no keyword arguments are given, an ArgumentError will be raised. + # + # If +self+ is frozen, the copy will also be frozen. + def with(**attrs) + attrs.empty? and + raise ArgumentError, "expected keyword arguments, none given" + copy = new(**attrs) + copy.freeze if frozen? + block_given? ? yield(copy) : copy + end + + # :call-seq: to_h -> hash + # + # Returns all config attributes in a hash. + def to_h; data.members.to_h { [_1, send(_1)] } end + @default = new( debug: false, open_timeout: 30, diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index b3bbe9d6..9391eadb 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -209,4 +209,73 @@ class ConfigTest < Test::Unit::TestCase assert child.inherited?(:idle_response_timeout) end + test "#to_h" do + expected = { + debug: false, open_timeout: 30, idle_response_timeout: 5, sasl_ir: true, + } + attributes = Config::AttrAccessors::Struct.members + default_hash = Config.default.to_h + assert_equal expected, default_hash.slice(*expected.keys) + assert_equal attributes, default_hash.keys + global_hash = Config.global.to_h + assert_equal attributes, global_hash.keys + assert_equal expected, global_hash.slice(*expected.keys) + end + + test "#update" do + config = Config.global.update(debug: true, sasl_ir: false, open_timeout: 2) + assert_same Config.global, config + assert_same true, config.debug + assert_same false, config.sasl_ir + assert_same 2, config.open_timeout + end + + # It's simple to check first that the names are valid, so we do. + test "#update with invalid key name" do + config = Config.new(debug: true, sasl_ir: false, open_timeout: 2) + assert_raise(ArgumentError) do + config.update(debug: false, sasl_ir: true, bogus: :invalid) + end + assert_same true, config.debug? + assert_same false, config.sasl_ir? + assert_same 2, config.open_timeout + end + + # Current behavior: partial updates are applied, in order they're received. + # We could make #update atomic, but the complexity probably isn't worth it. + test "#update with invalid value" do + config = Config.new(debug: true, sasl_ir: false, open_timeout: 2) + assert_raise(TypeError) do + config.update(debug: false, open_timeout: :bogus, sasl_ir: true) + end + assert_same false, config.debug? # updated + assert_same 2, config.open_timeout # unchanged + assert_same false, config.sasl_ir? # unchanged + end + + test "#with" do + orig = Config.new(open_timeout: 123, sasl_ir: false) + assert_raise(ArgumentError) do + orig.with + end + copy = orig.with(open_timeout: 456, idle_response_timeout: 789) + refute copy.frozen? + assert_same orig, copy.parent + assert_equal 123, orig.open_timeout # unchanged + assert_equal 456, copy.open_timeout + assert_equal 789, copy.idle_response_timeout + vals = nil + result = orig.with(open_timeout: 99, idle_response_timeout: 88) do |c| + vals = [c.open_timeout, c.idle_response_timeout, c.frozen?] + :result + end + assert_equal :result, result + assert_equal [99, 88, false], vals + orig.freeze + result = orig.with(open_timeout: 11) do |c| + vals = [c.open_timeout, c.idle_response_timeout, c.frozen?] + end + assert_equal [11, 5, true], vals + end + end