lib/dry/configurable/setting.rb
# frozen_string_literal: true require "set" require "dry/core/equalizer" require "dry/configurable/constants" require "dry/configurable/config" module Dry module Configurable # This class represents a setting and is used internally. # # @api private class Setting include Dry::Equalizer(:name, :value, :options, inspect: false) OPTIONS = %i[input default reader constructor cloneable settings].freeze DEFAULT_CONSTRUCTOR = -> v { v }.freeze CLONEABLE_VALUE_TYPES = [Array, Hash, Set, Config].freeze # @api private attr_reader :name # @api private attr_reader :writer_name # @api private attr_reader :input # @api private attr_reader :default # @api private attr_reader :options # Specialized Setting which includes nested settings # # @api private class Nested < Setting CONSTRUCTOR = Config.method(:new) # @api private def pristine with(input: input.pristine) end # @api private def constructor CONSTRUCTOR end end # @api private def self.cloneable_value?(value) CLONEABLE_VALUE_TYPES.any? { |type| value.is_a?(type) } end # @api private def initialize(name, input: Undefined, default: Undefined, **options) @name = name @writer_name = :"#{name}=" @options = options # Setting collections (see `Settings`) are shared between the configurable class # and its `config` object, so for cloneable individual settings, we duplicate # their _values_ as early as possible to ensure no impact from unintended mutation @input = input @default = default if cloneable? @input = input.dup @default = default.dup end evaluate if input_defined? end # @api private def input_defined? !input.equal?(Undefined) end # @api private def value return @value if evaluated? @value = constructor[Undefined.coalesce(input, default, nil)] end alias_method :evaluate, :value private :evaluate # @api private def evaluated? instance_variable_defined?(:@value) end # @api private def nested(settings) Nested.new(name, input: settings, **options) end # @api private def pristine with(input: Undefined) end # @api private def finalize!(freeze_values: false) if value.is_a?(Config) value.finalize!(freeze_values: freeze_values) elsif freeze_values value.freeze end freeze end # @api private def with(new_opts) self.class.new(name, input: input, default: default, **options, **new_opts) end # @api private def constructor options[:constructor] || DEFAULT_CONSTRUCTOR end # @api private def reader? options[:reader].equal?(true) end # @api private def writer?(meth) writer_name.equal?(meth) end # @api private def cloneable? if options.key?(:cloneable) # Return cloneable option if explicitly set options[:cloneable] else # Otherwise, infer cloneable from any of the input, default, or value Setting.cloneable_value?(input) || Setting.cloneable_value?(default) || ( evaluated? && Setting.cloneable_value?(value) ) end end private # @api private def initialize_copy(source) super @options = source.options.dup if source.cloneable? @input = source.input.dup @default = source.default.dup @value = source.value.dup if source.evaluated? end end end end end