Skip to content

Commit

Permalink
🔀 Merge pull request #302 from nevans/config/add-versioned_defaults
Browse files Browse the repository at this point in the history
🔧 Add versioned defaults
  • Loading branch information
nevans authored Jun 22, 2024
2 parents fa78037 + be8d5cd commit 394452e
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 4 deletions.
120 changes: 116 additions & 4 deletions lib/net/imap/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
54 changes: 54 additions & 0 deletions test/net/imap/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 394452e

Please sign in to comment.