336 lines
13 KiB
Ruby
336 lines
13 KiB
Ruby
# Copyright (c) HashiCorp, Inc.
|
|
# SPDX-License-Identifier: BUSL-1.1
|
|
|
|
module VagrantPlugins
|
|
module CommandServe
|
|
class Mappers
|
|
module Internal
|
|
class Graph
|
|
# Represent given mappers and inputs as
|
|
# graph.
|
|
class Mappers
|
|
include Util::HasLogger
|
|
# Weight given to root vertex
|
|
ROOT_WEIGHT = 0
|
|
# Weight given to source vertex or initial value
|
|
SOURCE_WEIGHT = 0
|
|
# Weight given to the destination vertex
|
|
FINAL_WEIGHT = 0
|
|
# Weight given to output types matching final
|
|
DST_WEIGHT = 0
|
|
# Weight given to named input value vertices
|
|
NAMED_VALUE_WEIGHT = 0
|
|
# Weight given to input value vertices
|
|
VALUE_WEIGHT = 10
|
|
# Weight given to input vertices
|
|
INPUT_WEIGHT = 20
|
|
# Weight given to output vertices
|
|
OUTPUT_WEIGHT = 20
|
|
|
|
# @return [Graph] graph instance representing mappers
|
|
attr_reader :graph
|
|
# @return [Object] source input value
|
|
attr_reader :source
|
|
# @return [Array<Object>] input values
|
|
attr_reader :inputs
|
|
# @return [String] named input to prefer
|
|
attr_reader :named
|
|
# @return [Mappers] mappers instance executing against
|
|
attr_reader :mappers
|
|
# @return [Class] expected return type
|
|
attr_reader :final
|
|
# @return [Boolean] graph is fresh (not using cached vertex list)
|
|
attr_reader :fresh
|
|
# @return [Array<Proc>] list of callables used for Method vertices
|
|
attr_reader :callables
|
|
|
|
@previous = {}
|
|
|
|
class << self
|
|
# Register a valid path for a given source
|
|
# and destination
|
|
#
|
|
# @param src [Class] source type
|
|
# @param dst [Class] destination type
|
|
# @param path [Array<Vertex>]
|
|
def register(src, dst, path)
|
|
begin
|
|
@previous[generate_key(src, dst)] = path
|
|
rescue KeyError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Remove an existing path registration
|
|
#
|
|
# @param src [Class] source type
|
|
# @param dst [Class] destination type
|
|
# @return [NilClass]
|
|
def unregister(src, dst)
|
|
@previous.delete(generate_key(src, dst))
|
|
nil
|
|
end
|
|
|
|
# Fetch a path for a given source and destination
|
|
# if it has been registered
|
|
#
|
|
# @param src [Class] source type
|
|
# @param dst [Class] destination type
|
|
# @return [Array<Vertex>, NilClass]
|
|
def previous(src, dst)
|
|
begin
|
|
@previous[generate_key(src, dst)]
|
|
rescue KeyError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Generate lookup key for given source
|
|
# and destination
|
|
#
|
|
# @param src [Class] source
|
|
# @param dst [Class] destination
|
|
# @return [String]
|
|
def generate_key(src, dst)
|
|
"#{src} -> #{dst}"
|
|
end
|
|
end
|
|
|
|
# Wrap a mappers instance into a graph with input values
|
|
# and determine and execute required path for desired output
|
|
#
|
|
# @param output_type [Class] Expected return type
|
|
# @param input_values [Array<Object>] Values provided for execution
|
|
# @param mappers [Mappers] Mappers instance to use
|
|
# @param named [String] Named input to prefer
|
|
# @param source [Object] Source value for conversion (optional)
|
|
def initialize(output_type:, input_values:, mappers:, named: nil, source: nil)
|
|
if !output_type.nil? && !output_type.is_a?(Class) && !output_type.is_a?(Module)
|
|
raise TypeError,
|
|
"Expected output type to be `Class', got `#{output_type.class}' (#{output_type})"
|
|
end
|
|
@final = output_type
|
|
@source = source
|
|
@inputs = Array(input_values)
|
|
if !mappers.is_a?(CommandServe::Mappers)
|
|
raise TypeError,
|
|
"Expected mapper to be `Mappers', got `#{mappers.class}'"
|
|
end
|
|
@mappers = mappers
|
|
@callables = mappers.mappers
|
|
@named = named.to_s
|
|
@fresh = true
|
|
|
|
# If we have a valid source type (and are not generating a value) check
|
|
# if a path has already been registered for this source/destination
|
|
# pair. If it has, we can fetch the callables out of the registered
|
|
# path and build our graph only using that subset of callables
|
|
if source != GENERATE_CLASS && self.class.previous(source, final)
|
|
@callables = self.class.previous(source, final).find_all { |v|
|
|
v.is_a?(Vertex::Method)
|
|
}.map(&:callable)
|
|
# Since we built the graph using a registered path lookup, mark
|
|
# this as non-fresh
|
|
@fresh = false
|
|
end
|
|
|
|
setup!
|
|
|
|
logger.debug { "new graph mappers instance created #{self}" }
|
|
logger.debug { "graph: #{graph.inspect}" }
|
|
end
|
|
|
|
# Generate path and execute required mappers
|
|
#
|
|
# @return [Object] result
|
|
def execute
|
|
# Generate list of vertices to reach destination
|
|
# from root, if possible
|
|
search = Search.new(graph: graph)
|
|
logger.debug { "searching for conversion path #{source ? source : inputs.first.class} -> #{final}" }
|
|
p = search.path(@root, @dst)
|
|
|
|
logger.debug {
|
|
sp = p.map{ |v|
|
|
v = v.to_s.slice(0, 100) + "...>" if v.to_s.length > 100
|
|
v
|
|
}.join(" ->\n ")
|
|
"found execution path:\n #{sp}"
|
|
}
|
|
|
|
# Call root first and validate it was
|
|
# actually root. The value is a stub,
|
|
# so it's not saved.
|
|
result = p.shift.call
|
|
if result != :root
|
|
raise "Initial vertex is not root. Expected `:root', got `#{result}'"
|
|
end
|
|
|
|
# If we were not generating a value and the graph was fresh,
|
|
# register this path lookup for future use.
|
|
if source != GENERATE_CLASS && fresh
|
|
self.class.register(source, final, p)
|
|
end
|
|
|
|
# Execute each vertex in the path
|
|
p.each do |v|
|
|
# The argument list required by the current
|
|
# vertex will be defined by its incoming edges
|
|
args = search.graph.in_vertices(v).map(&:value)
|
|
v.call(*args)
|
|
end
|
|
|
|
# The resultant value will be stored within the
|
|
# destination vertex
|
|
@dst.value
|
|
rescue => err
|
|
# If a failure was encountered and the graph was fresh
|
|
# allow the error to continue bubbling up
|
|
raise if fresh
|
|
|
|
# If the graph is not fresh, unregister the cached path
|
|
# and retry the mapping with the full graph
|
|
logger.trace("search execution using cached path failed, retrying with full graph (#{err})")
|
|
self.class.unregister(source, final)
|
|
self.class.new(
|
|
output_type: final,
|
|
input_values: inputs,
|
|
mappers: mappers,
|
|
named: named,
|
|
source: source
|
|
).execute
|
|
end
|
|
|
|
def to_s
|
|
"<Graph:Mappers output_type=#{final} input_values=#{inputs.map(&:class)}>"
|
|
end
|
|
|
|
def inspect
|
|
"<#{self.class.name}:#{object_id} output_type=#{final} input_values=#{inputs} graph=#{graph}>"
|
|
end
|
|
|
|
protected
|
|
|
|
# Setup the graph using the provided Mappers instance
|
|
def setup!
|
|
@graph = Graph.new
|
|
# Create a root vertex to provide a single starting point
|
|
@root = graph.add_vertex(Graph::Vertex::Root.new(value: :root))
|
|
@root.weight = ROOT_WEIGHT
|
|
# Add the provided input values
|
|
input_vertices = []
|
|
initial_value = true
|
|
|
|
input_vertices += inputs.map do |input_value|
|
|
if input_value == GENERATE
|
|
initial_value = false
|
|
next
|
|
end
|
|
if input_value.is_a?(Type::NamedArgument)
|
|
iv = graph.add_vertex(
|
|
Graph::Vertex::NamedValue.new(
|
|
name: input_value.name.to_s,
|
|
value: input_value.value
|
|
)
|
|
)
|
|
iv.weight = input_value.name.to_s == named ? NAMED_VALUE_WEIGHT : VALUE_WEIGHT
|
|
else
|
|
iv = graph.add_vertex(Graph::Vertex::Value.new(value: input_value))
|
|
iv.weight = initial_value ? SOURCE_WEIGHT : VALUE_WEIGHT
|
|
end
|
|
# If this is the initial value and we are not generating a result,
|
|
# then mark it as the origin value
|
|
if initial_value
|
|
@origin = iv
|
|
logger.info("origin vertex has been set: #{@origin}")
|
|
end
|
|
|
|
initial_value = false
|
|
graph.add_edge(@root, iv)
|
|
iv
|
|
end.compact
|
|
# Also add the known values from the mappers instance
|
|
input_vertices += mappers.known_arguments.map do |input_value|
|
|
iv = graph.add_vertex(Graph::Vertex::Value.new(value: input_value))
|
|
iv.weight = VALUE_WEIGHT
|
|
graph.add_edge(@root, iv)
|
|
iv
|
|
end
|
|
fn_inputs = Array.new
|
|
fn_outputs = Array.new
|
|
# Create vertices for all our registered mappers,
|
|
# as well as their inputs and outputs
|
|
callables.each do |mapper|
|
|
fn = graph.add_vertex(Graph::Vertex::Method.new(callable: mapper))
|
|
fn_inputs += mapper.inputs.map do |i|
|
|
iv = graph.add_vertex(Graph::Vertex::Input.new(type: i.type, origin_restricted: i.origin_restricted))
|
|
iv.weight = INPUT_WEIGHT + fn.extra_weight
|
|
graph.add_edge(iv, fn)
|
|
iv
|
|
end
|
|
ov = graph.add_vertex(Graph::Vertex::Output.new(type: mapper.output))
|
|
ov.weight = (mapper.output == final ? DST_WEIGHT : OUTPUT_WEIGHT) + fn.extra_weight
|
|
graph.add_edge(fn, ov)
|
|
fn_outputs << ov
|
|
end
|
|
# Create an output vertex for our expected
|
|
# result type
|
|
@dst = graph.add_vertex(Graph::Vertex::Final.new(type: final))
|
|
@dst.weight = FINAL_WEIGHT
|
|
|
|
# Add an edge from all our value vertices to
|
|
# matching input vertices
|
|
input_vertices.each do |iv|
|
|
fn_inputs.each do |f_iv|
|
|
if f_iv.origin_value_only? && iv != @origin
|
|
next
|
|
end
|
|
if iv.type == f_iv.type || iv.type.ancestors.include?(f_iv.type)
|
|
graph.add_edge(iv, f_iv)
|
|
end
|
|
end
|
|
|
|
# If a value vertex matches the desired
|
|
# output value, connect it directly
|
|
if @dst.type == iv.type # || iv.type.ancestors.include?(@dst.type)
|
|
graph.add_edge(iv, @dst)
|
|
end
|
|
end
|
|
# Add an edge from all our output vertices to
|
|
# matching input vertices
|
|
fn_outputs.each do |f_ov|
|
|
fn_inputs.each do |f_iv|
|
|
next if f_iv.origin_value_only?
|
|
if f_ov.type == f_iv.type || f_ov.type.ancestors.include?(f_iv.type)
|
|
graph.add_edge(f_ov, f_iv)
|
|
end
|
|
end
|
|
|
|
# If an output value matches the desired
|
|
# output value, connect it directly
|
|
if @dst.type == f_ov.type || f_ov.type.ancestors.include?(@dst)
|
|
if @dst.type != f_ov.type
|
|
f_ov.weight += 1000
|
|
end
|
|
graph.add_edge(f_ov, @dst)
|
|
end
|
|
end
|
|
|
|
# The output attached to our destination should be
|
|
# terminated at the destiation. If it has any out
|
|
# edges, remove those vertices (which has the bonus
|
|
# of helping to prevent cycles)
|
|
graph.each_in_vertex(@dst) do |vrt|
|
|
graph.each_out_vertex(vrt) do |ov|
|
|
next if ov == @dst
|
|
graph.remove_edge(vrt, ov)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|