# frozen_string_literal: truerequire'set'require'tsort'require'molinillo/dependency_graph/log'require'molinillo/dependency_graph/vertex'moduleMolinillo# A directed acyclic graph that is tuned to hold named dependenciesclassDependencyGraphincludeEnumerable# Enumerates through the vertices of the graph.# @return [Array<Vertex>] The graph's vertices.defeachreturnvertices.values.eachunlessblock_given?vertices.values.each{|v|yieldv}endincludeTSort# @!visibility privatealiastsort_each_nodeeach# @!visibility privatedeftsort_each_child(vertex,&block)vertex.successors.each(&block)end# Topologically sorts the given vertices.# @param [Enumerable<Vertex>] vertices the vertices to be sorted, which must# all belong to the same graph.# @return [Array<Vertex>] The sorted vertices.defself.tsort(vertices)TSort.tsort(lambda{|b|vertices.each(&b)},lambda{|v,&b|(v.successors&vertices).each(&b)})end# A directed edge of a {DependencyGraph}# @attr [Vertex] origin The origin of the directed edge# @attr [Vertex] destination The destination of the directed edge# @attr [Object] requirement The requirement the directed edge representsEdge=Struct.new(:origin,:destination,:requirement)# @return [{String => Vertex}] the vertices of the dependency graph, keyed# by {Vertex#name}attr_reader:vertices# @return [Log] the op log for this graphattr_reader:log# Initializes an empty dependency graphdefinitialize@vertices={}@log=Log.newend# Tags the current state of the dependency as the given tag# @param [Object] tag an opaque tag for the current state of the graph# @return [Void]deftag(tag)log.tag(self,tag)end# Rewinds the graph to the state tagged as `tag`# @param [Object] tag the tag to rewind to# @return [Void]defrewind_to(tag)log.rewind_to(self,tag)end# Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices}# are properly copied.# @param [DependencyGraph] other the graph to copy.definitialize_copy(other)super@vertices={}@log=other.log.duptraverse=lambdado|new_v,old_v|returnifnew_v.outgoing_edges.size==old_v.outgoing_edges.sizeold_v.outgoing_edges.eachdo|edge|destination=add_vertex(edge.destination.name,edge.destination.payload)add_edge_no_circular(new_v,destination,edge.requirement)traverse.call(destination,edge.destination)endendother.vertices.eachdo|name,vertex|new_vertex=add_vertex(name,vertex.payload,vertex.root?)new_vertex.explicit_requirements.replace(vertex.explicit_requirements)traverse.call(new_vertex,vertex)endend# @return [String] a string suitable for debuggingdefinspect"#{self.class}:#{vertices.values.inspect}"end# @return [String] Returns a dot format representation of the graphdefto_dotdot_vertices=[]dot_edges=[]vertices.eachdo|n,v|dot_vertices<<" #{n} [label=\"{#{n}|#{v.payload}}\"]"v.outgoing_edges.eachdo|e|dot_edges<<" #{e.origin.name} -> #{e.destination.name} [label=\"#{e.requirement}\"]"endenddot_vertices.sort!dot_edges.sort!dot=dot_vertices.unshift('digraph G {').push('')+dot_edges.push('}')dot.join("\n")end# @return [Boolean] whether the two dependency graphs are equal, determined# by a recursive traversal of each {#root_vertices} and its# {Vertex#successors}def==(other)returnfalseunlessothervertices.eachdo|name,vertex|other_vertex=other.vertex_named(name)returnfalseunlessother_vertexreturnfalseunlessother_vertex.successors.map(&:name).to_set==vertex.successors.map(&:name).to_setendend# @param [String] name# @param [Object] payload# @param [Array<String>] parent_names# @param [Object] requirement the requirement that is requiring the child# @return [void]defadd_child_vertex(name,payload,parent_names,requirement)root=!parent_names.delete(nil){true}vertex=add_vertex(name,payload,root)parent_names.eachdo|parent_name|parent_node=vertex_named(parent_name)add_edge(parent_node,vertex,requirement)endvertexend# Adds a vertex with the given name, or updates the existing one.# @param [String] name# @param [Object] payload# @return [Vertex] the vertex that was added to `self`defadd_vertex(name,payload,root=false)log.add_vertex(self,name,payload,root)end# Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively# removing any non-root vertices that were orphaned in the process# @param [String] name# @return [void]defdetach_vertex_named(name)log.detach_vertex_named(self,name)end# @param [String] name# @return [Vertex,nil] the vertex with the given namedefvertex_named(name)vertices[name]end# @param [String] name# @return [Vertex,nil] the root vertex with the given namedefroot_vertex_named(name)vertex=vertex_named(name)vertexifvertex&&vertex.root?end# Adds a new {Edge} to the dependency graph# @param [Vertex] origin# @param [Vertex] destination# @param [Object] requirement the requirement that this edge represents# @return [Edge] the added edgedefadd_edge(origin,destination,requirement)ifdestination.path_to?(origin)raiseCircularDependencyError.new([origin,destination])endadd_edge_no_circular(origin,destination,requirement)end# Sets the payload of the vertex with the given name# @param [String] name the name of the vertex# @param [Object] payload the payload# @return [Void]defset_payload(name,payload)log.set_payload(self,name,payload)endprivate# Adds a new {Edge} to the dependency graph without checking for# circularity.# @param (see #add_edge)# @return (see #add_edge)defadd_edge_no_circular(origin,destination,requirement)log.add_edge_no_circular(self,origin.name,destination.name,requirement)endendend