diff --git a/lib/net/imap/config.rb b/lib/net/imap/config.rb index 10740ffb..0b0c028d 100644 --- a/lib/net/imap/config.rb +++ b/lib/net/imap/config.rb @@ -54,25 +54,112 @@ class IMAP # plain_client.config.inherited?(:debug) # => true # plain_client.config.debug? # => false # + # == Versioned defaults + # + # The effective default configuration for a specific +x.y+ version of + # +net-imap+ can be loaded with the +config+ keyword argument to + # Net::IMAP.new. Requesting default configurations for previous versions + # enables extra backward compatibility with those versions: + # + # client = Net::IMAP.new(hostname, config: 0.3) + # client.config.sasl_ir # => false + # client.config.responses_without_block # => :silence_deprecation_warning + # + # client = Net::IMAP.new(hostname, config: 0.4) + # client.config.sasl_ir # => true + # client.config.responses_without_block # => :silence_deprecation_warning + # + # client = Net::IMAP.new(hostname, config: 0.5) + # client.config.sasl_ir # => true + # client.config.responses_without_block # => :warn + # + # client = Net::IMAP.new(hostname, config: :future) + # client.config.sasl_ir # => true + # client.config.responses_without_block # => :raise + # + # The versioned default configs inherit certain specific config options from + # Config.global, for example #debug: + # + # client = Net::IMAP.new(hostname, config: 0.4) + # Net::IMAP.debug = false + # client.config.debug? # => false + # + # Net::IMAP.debug = true + # client.config.debug? # => true + # + # === Named defaults + # In addition to +x.y+ version numbers, the following aliases are supported: + # + # [+:default+] + # An alias for +:current+. + # + # >>> + # *NOTE*: This is _not_ the same as Config.default. It inherits some + # attributes from Config.global, for example: #debug. + # [+:current+] + # An alias for the current +x.y+ version's defaults. + # [+:next+] + # The _planned_ config for the next +x.y+ version. + # [+:future+] + # The _planned_ eventual config for some future +x.y+ version. + # + # For example, to raise exceptions for all current deprecations: + # client = Net::IMAP.new(hostname, config: :future) + # client.responses # raises an ArgumentError # # == Thread Safety # # *NOTE:* Updates to config objects are not synchronized for thread-safety. # class Config + # Array of attribute names that are _not_ loaded by #load_defaults. + DEFAULT_TO_INHERIT = %i[debug].freeze + private_constant :DEFAULT_TO_INHERIT + # The default config, which is hardcoded and frozen. def self.default; @default end # The global config object. Also available from Net::IMAP.config. def self.global; @global end - def self.[](config) # :nodoc: unfinished API + # A hash of hard-coded configurations, indexed by version number. + def self.version_defaults; @version_defaults end + @version_defaults = {} + + # :call-seq: + # Net::IMAP::Config[number] -> versioned config + # Net::IMAP::Config[symbol] -> named config + # Net::IMAP::Config[hash] -> new frozen config + # Net::IMAP::Config[config] -> same config + # + # Given a version number, returns the default configuration for the target + # version. See Config@Versioned+defaults. + # + # Given a version name, returns the default configuration for the target + # version. See Config@Named+defaults. + # + # Given a Hash, creates a new _frozen_ config which inherits from + # Config.global. Use Config.new for an unfrozen config. + # + # Given a config, returns that same config. + def self.[](config) if config.is_a?(Config) || config.nil? && global.nil? config + elsif config.respond_to?(:to_hash) + new(global, **config).freeze else - raise TypeError, "no implicit conversion of %s to %s" % [ - config.class, Config - ] + version_defaults.fetch(config) { + case config + when Numeric + raise RangeError, "unknown config version: %p" % [config] + when Symbol + raise KeyError, "unknown config name: %p" % [config] + else + raise TypeError, "no implicit conversion of %s to %s" % [ + config.class, Config + ] + end + } end end @@ -198,6 +285,31 @@ def to_h; data.members.to_h { [_1, send(_1)] } end @global = default.new + version_defaults[0.4] = Config[ + default.to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) } + ] + + version_defaults[0] = Config[0.4].dup.update( + sasl_ir: false, + ).freeze + version_defaults[0.0] = Config[0] + version_defaults[0.1] = Config[0] + version_defaults[0.2] = Config[0] + version_defaults[0.3] = Config[0] + + version_defaults[0.5] = Config[0.4].dup.update( + responses_without_block: :warn, + ).freeze + + version_defaults[:default] = Config[0.4] + version_defaults[:current] = Config[0.4] + version_defaults[:next] = Config[0.5] + + version_defaults[:future] = Config[0.5].dup.update( + responses_without_block: :raise, + ).freeze + + version_defaults.freeze end end end diff --git a/test/net/imap/test_config.rb b/test/net/imap/test_config.rb index 9391eadb..8176f725 100644 --- a/test/net/imap/test_config.rb +++ b/test/net/imap/test_config.rb @@ -135,10 +135,64 @@ class ConfigTest < Test::Unit::TestCase assert_equal false, child.debug? end + test ".version_defaults are all frozen, and inherit debug from global" do + Config.version_defaults.each do |name, config| + assert [0, Float, Symbol].any? { _1 === name } + assert_kind_of Config, config + assert config.frozen?, "#{name} isn't frozen" + assert config.inherited?(:debug), "#{name} doesn't inherit debug" + assert_same Config.global, config.parent + end + end + + test ".[] for all x.y versions" do + original = Config[0] + assert_kind_of Config, original + assert_same original, Config[0.0] + assert_same original, Config[0.1] + assert_same original, Config[0.2] + assert_same original, Config[0.3] + assert_kind_of Config, Config[0.4] + assert_kind_of Config, Config[0.5] + end + + test ".[] range errors" do + assert_raise(RangeError) do Config[0.01] end + assert_raise(RangeError) do Config[0.11] end + assert_raise(RangeError) do Config[0.111] end + assert_raise(RangeError) do Config[0.9] end + assert_raise(RangeError) do Config[1] end + end + + test ".[] key errors" do + assert_raise(KeyError) do Config[:nonexistent] end + end + + test ".[] with symbol names" do + assert_same Config[0.4], Config[:current] + assert_same Config[0.4], Config[:default] + assert_same Config[0.5], Config[:next] + assert_kind_of Config, Config[:future] + end + + test ".[] with a hash" do + config = Config[{responses_without_block: :raise, sasl_ir: false}] + assert config.frozen? + refute config.sasl_ir? + assert config.inherited?(:debug) + refute config.inherited?(:sasl_ir) + assert_same Config.global, config.parent + assert_same :raise, config.responses_without_block + end + test ".new always sets a parent" do assert_same Config.global, Config.new.parent assert_same Config.default, Config.new(Config.default).parent assert_same Config.global, Config.new(Config.global).parent + assert_same Config[0.4], Config.new(0.4).parent + assert_same Config[0.5], Config.new(:next).parent + assert_equal true, Config.new({debug: true}, debug: false).parent.debug? + assert_equal true, Config.new({debug: true}, debug: false).parent.frozen? end test "#freeze" do