# frozen_string_literal: true require 'bolt/error' require 'bolt/util' module Bolt class Config module Transport class Base attr_reader :input def initialize(data = {}, boltdir = nil) assert_hash_or_config(data) @input = data @resolved = !Bolt::Util.references?(input) @config = resolved? ? Bolt::Util.deep_merge(defaults, filter(input)) : defaults @boltdir = boltdir validate if resolved? end # Accessor methods # These are mostly all wrappers for same-named Hash methods, but they all # require that the config options be fully-resolved before accessing data def [](key) resolved_config[key] end def to_h resolved_config end def fetch(*args) resolved_config.fetch(*args) end def include?(args) resolved_config.include?(args) end def dig(*keys) resolved_config.dig(*keys) end private def resolved_config unless resolved? raise Bolt::Error.new( "Unable to access transport config, #{self.class} has unresolved config: #{input.inspect}", 'bolt/unresolved-transport-config' ) end @config end # Merges the original input data with the provided data, which is either a hash # or transport config object. Accepts multiple inputs. def merge(*data) merged = data.compact.inject(@input) do |acc, layer| assert_hash_or_config(layer) layer_data = layer.is_a?(self.class) ? layer.input : layer Bolt::Util.deep_merge(acc, layer_data) end self.class.new(merged, @boltdir) end # Resolve any references in the input data, then remerge it with the defaults # and validate all values def resolve(plugins) @input = plugins.resolve_references(input) @config = Bolt::Util.deep_merge(defaults, filter(input)) @resolved = true validate end def resolved? @resolved end def self.options unless defined? self::OPTIONS raise NotImplementedError, "Constant OPTIONS must be implemented by the transport config class" end self::OPTIONS end private def defaults unless defined? self.class::DEFAULTS raise NotImplementedError, "Constant DEFAULTS must be implemented by the transport config class" end self.class::DEFAULTS end private def filter(unfiltered) unfiltered.slice(*self.class.options.keys) end private def assert_hash_or_config(data) return if data.is_a?(Hash) || data.is_a?(self.class) raise Bolt::ValidationError, "Transport config must be a Hash or #{self.class}, received #{data.class} #{data.inspect}" end private def normalize_interpreters(interpreters) Bolt::Util.walk_keys(interpreters) do |key| key.chars[0] == '.' ? key : '.' + key end end # Validation defaults to just asserting the option types private def validate assert_type end # Validates that each option is the correct type. Types are loaded from the OPTIONS hash. private def assert_type @config.each_pair do |opt, val| next unless (type = self.class.options.dig(opt, :type)) # Options that accept a Boolean value are indicated by the type TrueClass, so we # need some special handling here to check if the value is either true or false. if type == TrueClass unless [true, false].include?(val) raise Bolt::ValidationError, "#{opt} must be a Boolean true or false, received #{val.class} #{val.inspect}" end else unless val.nil? || val.is_a?(type) raise Bolt::ValidationError, "#{opt} must be a #{type}, received #{val.class} #{val.inspect}" end end end end end end end end