Add internal tools utilzing graph
This commit is contained in:
parent
0a26c040f7
commit
d81d505e8a
157
plugins/commands/serve/mappers/internal/graph/mappers.rb
Normal file
157
plugins/commands/serve/mappers/internal/graph/mappers.rb
Normal file
@ -0,0 +1,157 @@
|
||||
module VagrantPlugins
|
||||
module CommandServe
|
||||
class Mappers
|
||||
module Internal
|
||||
class Graph
|
||||
# Represent given mappers and inputs as
|
||||
# graph.
|
||||
class Mappers
|
||||
INPUT_WEIGHT = 0
|
||||
OUTPUT_WEIGHT = 10
|
||||
# @return [Graph] graph instance representing mappers
|
||||
attr_reader :graph
|
||||
# @return [Array<Object>] input values
|
||||
attr_reader :inputs
|
||||
# @return [Mappers] mappers instance executing against
|
||||
attr_reader :mappers
|
||||
# @return [Class] expected return type
|
||||
attr_reader :final
|
||||
|
||||
# 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
|
||||
def initialize(output_type:, input_values:, mappers:)
|
||||
if !output_type.is_a?(Class)
|
||||
raise TypeError,
|
||||
"Expected output type to be `Class', got `#{output_type.class}'"
|
||||
end
|
||||
@final = output_type
|
||||
@inputs = Array(input_values).compact
|
||||
if !mappers.is_a?(CommandServe::Mappers)
|
||||
raise TypeError,
|
||||
"Expected mapper to be `Mappers', got `#{mappers.class}'"
|
||||
end
|
||||
@mappers = mappers
|
||||
|
||||
setup!
|
||||
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)
|
||||
p = search.path(@root, @dst)
|
||||
|
||||
# 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
|
||||
|
||||
# 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.edges_in(v).map(&:value)
|
||||
v.call(*args)
|
||||
end
|
||||
|
||||
# The resultant value will be stored within the
|
||||
# destination vertex
|
||||
@dst.value
|
||||
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::Vertex.new(value: :root)
|
||||
# Add the provided input values
|
||||
input_vertices = inputs.map do |input_value|
|
||||
iv = Graph::Vertex::Value.new(value: input_value)
|
||||
graph.add_weighted_edge(@root, iv, INPUT_WEIGHT)
|
||||
iv
|
||||
end
|
||||
# Also add the known values from the mappers instance
|
||||
input_vertices += mappers.known_arguments.map do |input_value|
|
||||
iv = Graph::Vertex::Value.new(value: input_value)
|
||||
graph.add_weighted_edge(@root, iv, INPUT_WEIGHT)
|
||||
iv
|
||||
end
|
||||
fn_inputs = Array.new
|
||||
fn_outputs = Array.new
|
||||
# Create vertices for all our registered mappers,
|
||||
# as well as their inputs and outputs
|
||||
mappers.mappers.each do |mapper|
|
||||
fn = Graph::Vertex::Method.new(callable: mapper)
|
||||
fn_inputs += mapper.inputs.map do |i|
|
||||
iv = Graph::Vertex::Input.new(type: i.type)
|
||||
graph.add_edge(iv, fn)
|
||||
iv
|
||||
end
|
||||
ov = Graph::Vertex::Output.new(type: mapper.output)
|
||||
graph.add_edge(fn, ov)
|
||||
fn_outputs << ov
|
||||
end
|
||||
# Create an output vertex for our expected
|
||||
# result type
|
||||
@dst = Graph::Vertex::Output.new(type: final)
|
||||
# Add an edge from all our value vertices to
|
||||
# matching input vertices
|
||||
input_vertices.each do |iv|
|
||||
fn_inputs.each do |f_iv|
|
||||
if iv.type == f_iv.type
|
||||
if iv.type == Hashicorp::Vagrant::Sdk::FuncSpec::Value ||
|
||||
f_iv.type == Hashicorp::Vagrant::Sdk::FuncSpec::Value
|
||||
raise "wtf, #{self.inspect}"
|
||||
end
|
||||
graph.add_weighted_edge(iv, f_iv, INPUT_WEIGHT)
|
||||
end
|
||||
end
|
||||
|
||||
# If a value vertex matches the desired
|
||||
# output value, connect it directly
|
||||
if @dst.type == iv.type
|
||||
graph.add_weighted_edge(iv, @dst, INPUT_WEIGHT)
|
||||
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|
|
||||
if f_ov.type == f_iv.type
|
||||
if f_ov.type == Hashicorp::Vagrant::Sdk::FuncSpec::Value ||
|
||||
f_iv.type == Hashicorp::Vagrant::Sdk::FuncSpec::Value
|
||||
raise "wtf outs, #{self.inspect}"
|
||||
end
|
||||
|
||||
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
|
||||
graph.add_weighted_edge(f_ov, @dst, OUTPUT_WEIGHT)
|
||||
end
|
||||
end
|
||||
# Finalize the graphs so edges are properly
|
||||
# sorted by their weight
|
||||
graph.finalize!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
219
plugins/commands/serve/mappers/internal/graph/search.rb
Normal file
219
plugins/commands/serve/mappers/internal/graph/search.rb
Normal file
@ -0,0 +1,219 @@
|
||||
module VagrantPlugins
|
||||
module CommandServe
|
||||
class Mappers
|
||||
module Internal
|
||||
class Graph
|
||||
# Provides searching of a graph to determine if a
|
||||
# destination vertex is accessible from a given
|
||||
# root vertex.
|
||||
class Search
|
||||
class NoPathError < StandardError; end
|
||||
|
||||
# Value used for marking visited vertices
|
||||
VERTEX_ID = :vertex
|
||||
|
||||
attr_reader :graph, :root, :visited
|
||||
|
||||
# Create a new DFS instance
|
||||
#
|
||||
# @param graph [Graph] Graph used for searching
|
||||
def initialize(graph:)
|
||||
@graph = graph.copy
|
||||
@m = Mutex.new
|
||||
@root = nil
|
||||
@visited = nil
|
||||
end
|
||||
|
||||
# Provide a list of vertices not visited to build
|
||||
# the generated path. If no path has been generated,
|
||||
# it will return an empty value by default.
|
||||
#
|
||||
# @return [Array<Vertex>] list of vertices
|
||||
def orphans
|
||||
@m.synchronize do
|
||||
load_orphans
|
||||
end
|
||||
end
|
||||
|
||||
# Generate a path from the given source vertex to
|
||||
# the given destination vertex using a depth first
|
||||
# search algorithm.
|
||||
#
|
||||
# @param src [Vertex] Source vertex
|
||||
# @param dst [Vertex] Destination vertex
|
||||
# @return [Array<Vertex>] path from source to destination
|
||||
# @raises [NoPathError] when no path can be determined
|
||||
def path(src, dst)
|
||||
@m.synchronize do
|
||||
@root = src
|
||||
@visited = {}
|
||||
stack = Stack.new
|
||||
stack.push(src)
|
||||
|
||||
p = find_path(src, dst, stack)
|
||||
|
||||
if Array(p).empty?
|
||||
@visited = nil
|
||||
raise NoPathError,
|
||||
"failed to determine valid path"
|
||||
end
|
||||
|
||||
# Now ensure our final path is in the correct order
|
||||
# based on edge defined dependencies
|
||||
load_orphans.each do |v|
|
||||
graph.remove(v)
|
||||
end
|
||||
t = Topological.new(graph: graph)
|
||||
t.sort
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Find path from source to destination
|
||||
#
|
||||
# @param src [Vertex] Source vertex
|
||||
# @param dst [Vertex] Destination vertex
|
||||
# @param s [Stack] Stack for holding path information
|
||||
# @return [Array<Vertex>, nil] list of vertices or nil if not found
|
||||
def find_path(src, dst, s)
|
||||
# If we have reached our destination then it's
|
||||
# time to mark vertices included in the final
|
||||
# path as visited and return the path
|
||||
if src == dst
|
||||
p = s.values
|
||||
p.each do |v|
|
||||
visited[v.hash_code] = VERTEX_ID
|
||||
end
|
||||
return p
|
||||
end
|
||||
|
||||
graph.edges_out(src).each do |v|
|
||||
# If the incoming edges don't define dependencies
|
||||
# then we only care about a single path through
|
||||
# the vertex
|
||||
if !v.incoming_edges_required
|
||||
if s.include?(v)
|
||||
next
|
||||
end
|
||||
s.push(v)
|
||||
if p = find_path(v, dst, s)
|
||||
return p
|
||||
end
|
||||
s.pop
|
||||
next
|
||||
end
|
||||
|
||||
# Since the incoming edges define dependencies
|
||||
# we need to validate they are reachable from
|
||||
# root before we continue attempting to build
|
||||
# the path
|
||||
req_paths = []
|
||||
if graph.edges_out(v).size > 1
|
||||
d = self.class.new(graph: graph.reverse)
|
||||
begin
|
||||
p = d.path(dst, v)
|
||||
req_paths << p
|
||||
rescue NoPathError
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
failed = false
|
||||
graph.edges_in(v).each do |in_v|
|
||||
# This was our initial path here so ignore
|
||||
if src.hash_code == in_v.hash_code
|
||||
next
|
||||
end
|
||||
|
||||
# Create a new graph but reverse the direction
|
||||
new_g = graph.reverse
|
||||
# Remove edges from the vertex except for the
|
||||
# one we are currently processing
|
||||
new_g.edges_out(v).each do |out_v|
|
||||
if out_v.hash_code == in_v.hash_code
|
||||
next
|
||||
end
|
||||
new_g.remove_edge(v, out_v)
|
||||
end
|
||||
|
||||
# Create a new DFS to search using the reversed
|
||||
# graph
|
||||
new_d = self.class.new(graph: new_g)
|
||||
begin
|
||||
# Attempt to find a path from the vertex back
|
||||
# to root
|
||||
p = new_d.path(v, root)
|
||||
req_paths << p
|
||||
rescue NoPathError
|
||||
# If a path could not be found, mark failed
|
||||
# and bail
|
||||
failed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# If all the incoming edges couldn't be satisfied
|
||||
# then we ignore this vertex and move on
|
||||
if failed
|
||||
next
|
||||
end
|
||||
|
||||
# All the incoming edges could be satisfied so
|
||||
# we push this vertex on the stack and continue
|
||||
# down the current path
|
||||
s.push(v)
|
||||
|
||||
# If we were able to reach the destination on
|
||||
# this path, we now need to add in the extra
|
||||
# paths generated to satisfy the edge requirements
|
||||
if p = find_path(v, dst, s)
|
||||
# Store the position of the vertex we are processing
|
||||
# in the path so we can properly cut and glue the path
|
||||
pos = p.index(v)
|
||||
# Cut the path prior to our current vertex
|
||||
path_start = p.slice(0, pos)
|
||||
|
||||
# Now walk through each extra path and add any vertices
|
||||
# that have not yet been seen
|
||||
req_paths.each do |extra_path|
|
||||
# The extra path originated from a reversed graph, so
|
||||
# reverse this path before processing
|
||||
extra_path.reverse.each do |extra_v|
|
||||
if visited.key?(extra_v.hash_code)
|
||||
next
|
||||
end
|
||||
visited[extra_v.hash_code] = VERTEX_ID
|
||||
path_start << extra_v
|
||||
end
|
||||
end
|
||||
|
||||
# Now glue the remaining path to our modified start
|
||||
# path and return the result
|
||||
return path_start + p.slice(pos, p.size)
|
||||
end
|
||||
# If we made it here the vertex isn't part of a valid
|
||||
# path so pop it off the stack and continue
|
||||
s.pop
|
||||
end
|
||||
|
||||
# If we have nothing left to process, return nothing
|
||||
nil
|
||||
end
|
||||
|
||||
def load_orphans
|
||||
if visited.nil?
|
||||
return []
|
||||
end
|
||||
graph.vertices.map do |v|
|
||||
v if !visited.key?(v.hash_code)
|
||||
end.compact
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
81
plugins/commands/serve/mappers/internal/graph/topological.rb
Normal file
81
plugins/commands/serve/mappers/internal/graph/topological.rb
Normal file
@ -0,0 +1,81 @@
|
||||
module VagrantPlugins
|
||||
module CommandServe
|
||||
class Mappers
|
||||
module Internal
|
||||
class Graph
|
||||
# Provides topological sorting of a Graph
|
||||
class Topological
|
||||
class NoRootError < StandardError; end
|
||||
class CycleError < StandardError; end
|
||||
|
||||
attr_reader :graph
|
||||
|
||||
# Create a new topological sorting instance
|
||||
#
|
||||
# @param graph [Graph] Graph used for sorting
|
||||
def initialize(graph:)
|
||||
@graph = graph.copy
|
||||
@m = Mutex.new
|
||||
end
|
||||
|
||||
# Generate a topological sorted path of the defined
|
||||
# graph.
|
||||
#
|
||||
# @return [Array<Vertex>]
|
||||
# @raises [NoRootError, CycleError]
|
||||
def sort
|
||||
@m.synchronize do
|
||||
s = Stack.new
|
||||
|
||||
graph.vertices.each do |v|
|
||||
s.push(v) if graph.edges_in(v).size < 1
|
||||
end
|
||||
|
||||
if s.size < 1
|
||||
raise NoRootError,
|
||||
"graph does not contain any root vertices"
|
||||
end
|
||||
|
||||
kahn(s)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Sort the graph vertices using Kahn's algorithm
|
||||
#
|
||||
# @param s [Stack] Stack to hold vertices
|
||||
# @return [Array<Vertex>]
|
||||
# @raises [CycleError]
|
||||
def kahn(s)
|
||||
p = Queue.new
|
||||
while s.size > 0
|
||||
v = s.pop
|
||||
p.push(v)
|
||||
graph.edges_out(v).each do |next_v|
|
||||
graph.remove_edge(v, next_v)
|
||||
if graph.edges_in(next_v).size < 1
|
||||
s.push(next_v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
graph.vertices.each do |v|
|
||||
if graph.edges_in(v).size > 1 || graph.edges_out(v).size > 1
|
||||
raise CycleError,
|
||||
"graph contains at least one cycle"
|
||||
end
|
||||
end
|
||||
|
||||
Array.new.tap do |path|
|
||||
while p.size > 0
|
||||
path << p.pop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
x
Reference in New Issue
Block a user