# Copyright (c) HashiCorp, Inc. # SPDX-License-Identifier: BUSL-1.1 module VagrantPlugins module CommandServe class Mappers # Mapper defines a single mapping from a source # value to a destination value class Mapper extend Util::HasLogger # Track all known mappers @@mappers = [] @@init = false # @return [Array] list of known mappers def self.registered @@mappers end # Represents an input argument for a mapper class Input # @return [String] optional name for input attr_reader :name # @return [Class] type of the argument attr_reader :type # @return [#call] callable that can validate argument attr_reader :validator # @return [Boolean] Only allow the origin value to be connected to this input attr_reader :origin_restricted # Create a new input # # @param type [Class] Type of the input # @param validator [Callable] Callable to validate argument (optional) # @yield Callable to validate argument (optional) def initialize(type:, validator: nil, origin_restricted: false, &block) if !type.is_a?(Class) && !type.is_a?(Module) raise ArgumentError, "Type must be constant type (given: #{type})" end @type = type if validator && block raise ArgumentError, "Only one of `:validator' option or block may be used" end @validator = validator || block @validator = lambda{ |_| true } if !@validator if !@validator.respond_to?(:call) raise ArgumentError, "Validator must be callable" end @origin_restricted = origin_restricted end # Check if given argument is valid for this input # # @param arg [Object] Argument to validate # @return [Boolean] def valid?(arg) if !arg.is_a?(type) && arg != type return false end return true if arg.is_a?(Class) validator.call(arg) end end # Registers class as a known mapper def self.inherited(klass) @@mappers << klass end def self.generate_anys return if @@init @@mappers.each do |klass| # For any mapper that outputs a protobuf message, # automatically provide an Any mapper. m = klass.new if m.output.ancestors.include?(Google::Protobuf::MessageExts) names = registered.map(&:name) next if names.include?("#{m.output.name}ToAny") logger.trace { "generating new Any converter #{m.output.name}ToAny" } Class.new(Mapper).class_eval(" def self.name '#{m.output.name}' + 'ToAny' end def initialize super( inputs: [Input.new(type: #{m.output.name})], output: Google::Protobuf::Any, func: method(:converter) ) end def converter(v) Google::Protobuf::Any.pack(v) end def to_s '<#{m.output.name}' + 'ToAny:' + object_id.to_s + '>' end ") end end @@init = true end include Util::HasLogger # @return [Array] list of inputs for mapper attr_reader :inputs # @return [Class, nil] type of output attr_reader :output # @return [#call] callable to perform mapping attr_reader :func # Create a new mapper instance # # @param inputs [Array] List of inputs for mapper # @param output [Class] Type of output value # @param func [#call] Callable to perform mapping def initialize(inputs:, output:, func:) Array(inputs).each do |i| if !i.is_a?(Input) raise ArgumentError, "Inputs must be `Input' type (given: #{i.inspect})" end end @inputs = Array(inputs) if !output.is_a?(Class) raise ArgumentError, "Output must be Class type (given: #{output.inspect} / #{output.class})" end @output = output if !func.respond_to?(:call) raise ArgumentError, "Func must be callable" end @func = func end # Calls the mapper with the given arguments def call(*args) if args.size > inputs.size raise ArgumentError, "Expected `#{inputs.size}' arguments but received `#{args.size}'" end args.each_with_index do |a, i| if !inputs[i].valid?(a) raise ArgumentError, "Invalid argument provided `#{a.class}' for input `#{inputs[i].type}'" end end result = func.call(*args) if !result.is_a?(output) raise TypeError, "Expected output type of `#{output}', got `#{result.class}' (in #{self.class})" end result end # @return [Boolean] returns true arguments contain needed inputs def satisfied_by?(*args) begin determine_inputs(*args) true rescue ArgumentError false end end # Builds argument list for mapper call with given arguments # # @return [Array] def determine_inputs(*args) Array.new.tap do |found_inputs| inputs.each do |input| value = args.detect do |arg| input.valid?(arg) end if value.nil? && input.type != NilClass logger.error { "missing input for type `#{input.type}' - #{args.inspect}" } raise ArgumentError, "Failed to locate required argument of type `#{input.type}'" end found_inputs << value end end end end end end end