197 lines
6.3 KiB
Ruby
197 lines
6.3 KiB
Ruby
# 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<Class>] 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<Input>] 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<Input>] 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<Object>]
|
|
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
|