# frozen_string_literal: true require "set" module Dry module Configurable # Config exposes setting values through a convenient API # # @api public class Config include Dry::Equalizer(:values) # @api private attr_reader :_settings # @api private attr_reader :_values # @api private attr_reader :_configured protected :_configured # @api private def initialize(settings, values: {}) @_settings = settings @_values = values @_configured = Set.new end # @api private private def initialize_copy(source) super @_values = source.__send__(:dup_values) @_configured = source._configured.dup end # @api private def dup_for_settings(settings) dup.tap { |config| config.instance_variable_set(:@_settings, settings) } end # Get config value by a key # # @param [String,Symbol] name # # @return Config value def [](name) name = name.to_sym unless (setting = _settings[name]) raise ArgumentError, "+#{name}+ is not a setting name" end _values.fetch(name) { # Mutable settings may be configured after read _configured.add(name) if setting.cloneable? setting.to_value.tap { |value| _values[name] = value } } end # Set config value. # Note that finalized configs cannot be changed. # # @param [String,Symbol] name # @param [Object] value def []=(name, value) raise FrozenConfigError, "Cannot modify frozen config" if frozen? name = name.to_sym unless (setting = _settings[name]) raise ArgumentError, "+#{name}+ is not a setting name" end _configured.add(name) _values[name] = setting.constructor.(value) end # Update config with new values # # @param values [Hash, #to_hash] A hash with new values # # @return [Config] # # @api public def update(values) values.each do |key, value| if self[key].is_a?(self.class) unless value.respond_to?(:to_hash) raise ArgumentError, "#{value.inspect} is not a valid setting value" end self[key].update(value.to_hash) else self[key] = value end end self end # Returns true if the value for the given key has been set on this config. # # For simple values, this returns true if the value has been explicitly assigned. # # For cloneable (mutable) values, since these are captured on read, returns true if the value # does not compare equally to its corresdponing default value. This relies on these objects # having functioning `#==` checks. # # @return [Bool] # # @api public def configured?(key) if _configured.include?(key) && _settings[key].cloneable? return _values[key] != _settings[key].to_value end _configured.include?(key) end # Returns the current config values. # # Nested configs remain in their {Config} instances. # # @return [Hash] # # @api public def values # Ensure all settings are represented in values _settings.each { |setting| self[setting.name] unless _values.key?(setting.name) } _values end # Returns config values as a hash, with nested values also converted from {Config} instances # into hashes. # # @return [Hash] # # @api public def to_h values.to_h { |key, value| [key, value.is_a?(self.class) ? value.to_h : value] } end # @api private alias_method :_dry_equalizer_hash, :hash # @api public def hash return @__hash__ if instance_variable_defined?(:@__hash__) _dry_equalizer_hash end # @api public def finalize!(freeze_values: false) return self if frozen? values.each_value do |value| if value.is_a?(self.class) value.finalize!(freeze_values: freeze_values) elsif freeze_values value.freeze end end # Memoize the hash for the object when finalizing (regardless of whether values themselves # are to be frozen; the intention of finalization is that no further changes should be # made). The benefit of freezing the hash at this point is that it saves repeated expensive # computation (through Dry::Equalizer's hash implementation) if that hash is to be used # later in performance-sensitive situations, such as when serving as a cache key or similar. @__hash__ = _dry_equalizer_hash freeze end # @api private def pristine self.class.new(_settings) end private def method_missing(name, *args) setting_name = setting_name_from_method(name) setting = _settings[setting_name] super unless setting if name.end_with?("=") self[setting_name] = args[0] else self[setting_name] end end def respond_to_missing?(meth, include_private = false) _settings.key?(setting_name_from_method(meth)) || super end def setting_name_from_method(method_name) method_name.to_s.tr("=", "").to_sym end def dup_values _values.each_with_object({}) { |(key, val), dup_hsh| dup_hsh[key] = _settings[key].cloneable? ? val.dup : val } end end end end