2022-04-25 12:25:58 -05:00

218 lines
7.5 KiB
Ruby

require "digest/sha2"
module VagrantPlugins
module CommandServe
# Provides value mapping to ease interaction
# with protobuf and clients
class Mappers
include Util::HasLogger
autoload :Internal, Vagrant.source_root.join("plugins/commands/serve/mappers/internal").to_s
autoload :Mapper, Vagrant.source_root.join("plugins/commands/serve/mappers/mapper").to_s
# @return [Array<Object>] arguments provided to all mapper calls
attr_reader :known_arguments
# @return [Array<Mapper>] list of mappers
attr_reader :mappers
# @return [Util::Cacher] cached mapped values
attr_accessor :cacher
# Create a new mappers instance. Any arguments provided will be
# available to all mapper calls
def initialize(*args)
@known_arguments = args
Mapper.generate_anys
@mappers = Mapper.registered.map(&:new)
@identifiers = []
end
def clone
n = self.class.new(*known_arguments)
end
# Add an argument to be included with mapping calls
#
# @param v [Object] Argument value
# @return [Object]
def add_argument(v)
known_arguments << v
@identifiers << identify(v)
v
end
def cached(key)
return false if cacher.nil?
cacher[key]
end
def cache_key(*args)
key = Digest::SHA256.new
@identifiers.each { |i| key << i }
args.each do |a|
key << identify(a)
end
key.hexdigest
end
def identify(thing)
thing.respond_to?(:cacher_id) ?
thing.cacher_id.to_s :
thing.object_id.to_s
end
def unany(any)
type = any.type.split('/').last.to_s.split('.').inject(Object) { |memo, name|
memo.const_get(name.to_s.capitalize)
}
any.unpack(type)
end
# Map a given value
#
# @param value [Object] Value to map
# @param to [Class] Resultant type (optional)
# @return [Object]
def map(value, *extra_args, to: nil)
# If we already processed this value, return cached value
cache_key = cache_key(value, *extra_args, to)
if c = cached(cache_key)
return c
end
if value.nil? && to
val = (extra_args + known_arguments).detect do |item|
item.is_a?(to)
end
return val if val
end
if value.is_a?(Google::Protobuf::Any)
value = unany(value)
end
args = ([value] + extra_args + known_arguments).compact
result = nil
# For funcspec values, we want to pre-filter since they use
# a custom validator. This will prevent invalid paths.
if value.is_a?(SDK::FuncSpec::Value)
map_mapper = self.clone
valid_mappers = map_mapper.mappers.map do |m|
next if !m.inputs.first.valid?(SDK::FuncSpec::Value) &&
m.output.ancestors.include?(Google::Protobuf::MessageExts)
next m if !m.inputs.first.valid?(SDK::FuncSpec::Value) ||
m.inputs.first.valid?(value)
logger.warn("removing mapper - invalid funcspec match - #{m}")
nil
end.compact
map_mapper.mappers.replace(valid_mappers)
else
map_mapper = self
end
# If we don't have a desired final type, test for mappers
# that are satisfied by the arguments we have and run that
# directly
if to.nil?
valid_outputs = []
cb = lambda do |k|
matches = map_mapper.mappers.find_all do |m|
m.inputs.first.valid?(k)
end
outs = matches.map(&:output)
to_search = outs - valid_outputs
valid_outputs |= outs
to_search.each do |o|
cb.call(o)
end
end
cb.call(value)
if valid_outputs.empty?
raise TypeError,
"No valid mappers found for input type `#{value.class}' (#{value})"
end
valid_outputs.reverse!
logger.debug("mapper output types discovered for input type `#{value.class}': #{valid_outputs}, mappers:\n" + map_mapper.mappers.map(&:to_s).join("\n"))
last_error = nil
valid_outputs.each do |out|
begin
m_graph = Internal::Graph::Mappers.new(
output_type: out,
mappers: map_mapper,
input_values: args,
)
result = m_graph.execute
break
rescue => err
logger.debug("typeless mapping failure (non-critical): #{err} (input - #{value} / output #{out})")
last_error = err
end
end
raise last_error if result.nil? && last_error
else
m_graph = Internal::Graph::Mappers.new(
output_type: to,
mappers: map_mapper,
input_values: args,
)
result = m_graph.execute
end
cacher[cache_key] = result if cacher
logger.debug("map of #{value} to #{to.inspect} => #{result}")
result
end
# Generate the given type based on given and/or
# added arguments
def generate(*args, type:)
map(nil, *args, to: type)
end
# Map values provided by a FuncSpec request into
# actual values
#
# @param spec [SDK::FuncSpec::Spec]
# @param expect [Array<Class>] Expected types for each argument
# @return [Array<Object>, Object]
def funcspec_map(spec, *extra_args, expect: [])
expect = Array(expect)
expect.unshift(expect.pop).compact
result = Array.new.tap do |result_args|
spec.args.each_with_index do |arg, i|
result_args << map(arg, *extra_args + result_args, to: expect[i])
end
end
if result.size == 1
return result.first
end
# NOTE: the spec will have the order of the arguments
# shifted one. not sure why, but we can just work around
# it here for now.
result.push(result.shift)
end
end
end
end
# NOTE: Always directly load mappers so they are automatically registered and
# available. Using autoloading behavior will result in them being unaavailable
# until explicitly requested by name
require Vagrant.source_root.join("plugins/commands/serve/mappers/box.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/capabilities.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/command.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/direct.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/environment.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/guest.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/known_types.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/machine.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/project.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/state_bag.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/target_index.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/terminal.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/ui.rb").to_s
require Vagrant.source_root.join("plugins/commands/serve/mappers/wrappers.rb").to_s