require 'pdk' module PDK class Config # A class for describing the setting of a {PDK::Config} setting. # # Generally, this is never instantiated manually, but is instead # instantiated by passing a block to {PDK::Config::Namespace#setting}. # # @example # # PDK::Config::Namespace.new('module_defaults') do # setting :author do # validate PDK::Config::Validator.string # default_to { false } # end # end class Setting attr_reader :namespace # It is possible to have multiple setting definitions for the same setting; for example, defining a default value with a lambda, but the # the validation is within a JSON schema document. These are expressed as two settings objects, and uses a single linked list to join them # together: # # (PDK::Config::JSONSchemaSetting) --previous_setting--> (PDK::Config::Setting) # # So in the example above, calling `default` the on the first object in the list will: # 1. Look at `default` on PDK::Config::JSONSchemaSetting # 2. If a default could not be found then it calls `default` on previous_setting # 3. If a default could not be found then it calls `default` on previous_setting.previous_setting # 4. and so on down the linked list (chain) of settings attr_writer :previous_setting # Initialises an empty setting definition. # # @param name [String,Symbol] the name of the setting. # @param namespace [PDK::Config::Namespace] The namespace this setting belongs to def initialize(name, namespace, initial_value = nil) @name = name.to_s @validators = [] @namespace = namespace @value = initial_value end def qualified_name [namespace.name, @name].join('.') end def value # Duplicate arrays and hashes so that they are isolated from changes being made PDK::Util.deep_duplicate(@value) end def value=(obj) validate!(obj) @value = obj end def to_s @value.to_s end # Assign a validator to the setting. Subclasses should not override this method. # # @param validator [Hash{Symbol => [Proc,String]}] # @option validator [Proc] :proc a lambda that takes the setting to be # validated as the argument and returns `true` if the setting is valid. # @option validator [String] :message a description of what the validator # is testing for, that is displayed to the user as part of the error # message for invalid settings. # # @raise [ArgumentError] if not passed a Hash. # @raise [ArgumentError] if the Hash doesn't have a `:proc` key that # contains a Proc. # @raise [ArgumentError] if the Hash doesn't have a `:message` key that # contains a String. # # @return [nil] def validate(validator) raise ArgumentError, '`validator` must be a Hash' unless validator.is_a?(Hash) raise ArgumentError, 'the :proc key must contain a Proc' unless validator.key?(:proc) && validator[:proc].is_a?(Proc) raise ArgumentError, 'the :message key must contain a String' unless validator.key?(:message) && validator[:message].is_a?(String) @validators << validator end # Validate a setting against the assigned validators. # # @param setting [Object] the setting being validated. # # @raise [ArgumentError] if any of the assigned validators fail to # validate the setting. # # @return [nil] def validate!(value) @validators.each do |validator| next if validator[:proc].call(value) raise ArgumentError, format('%{key} %{message}', key: qualified_name, message: validator[:message]) end end # Assign a default value proc for the setting. Subclasses should not override this method. # # @param block [Proc] a block that is lazy evaluated when necessary in # order to determine the default setting. # # @return [nil] def default_to(&block) raise ArgumentError, 'must be passed a block' unless block @default_to = block end # Evaluate the default setting. # # @return [Object,nil] the result of evaluating the block given to # {#default_to}, or `nil` if the setting has no default. def default return @default_to.call if default_block? # If there is a previous setting in the chain, use its default @previous_setting&.default end private # @return [Boolean] true if the setting has a default setting block. Subclasses should not override this method. def default_block? !@default_to.nil? end end end end