turbine-graph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ require 'turbine'
2
+
3
+ module Turbine
4
+
5
+ # This example is taken from a TV show called 'Modern Family' with complex
6
+ # family relations, in other words: ideal to test a complex graph structure.
7
+ #
8
+ # http://en.wikipedia.org/wiki/List_of_Modern_Family_characters
9
+ def self.family_stub
10
+ graph = Turbine::Graph.new
11
+
12
+ phil = graph.add(Turbine::Node.new(:phil, gender: :male))
13
+ claire = graph.add(Turbine::Node.new(:claire, gender: :female))
14
+ haley = graph.add(Turbine::Node.new(:haley, gender: :female))
15
+ alex = graph.add(Turbine::Node.new(:alex, gender: :female))
16
+ luke = graph.add(Turbine::Node.new(:luke, gender: :male))
17
+
18
+ jay = graph.add(Turbine::Node.new(:jay, gender: :male))
19
+ gloria = graph.add(Turbine::Node.new(:gloria, gender: :female))
20
+ manny = graph.add(Turbine::Node.new(:manny, gender: :male))
21
+ unnamed = graph.add(Turbine::Node.new(:unnamed))
22
+
23
+ mitchell = graph.add(Turbine::Node.new(:mitchell, gender: :male))
24
+ cameron = graph.add(Turbine::Node.new(:cameron, gender: :male))
25
+ lily = graph.add(Turbine::Node.new(:lily, gender: :female))
26
+
27
+ dede = graph.add(Turbine::Node.new(:dede, gender: :female))
28
+ javier = graph.add(Turbine::Node.new(:javier, gender: :male))
29
+
30
+ frank = graph.add(Turbine::Node.new(:frank, gender: :male))
31
+ sarah = graph.add(Turbine::Node.new(:sarah, gender: :female))
32
+
33
+ # Dunphy -----------------------------------------------------------------
34
+
35
+ # Phil
36
+ phil.connect_to(claire, :spouse)
37
+ phil.connect_to(haley, :child)
38
+ phil.connect_to(alex, :child)
39
+ phil.connect_to(luke, :child)
40
+
41
+ # Claire
42
+ claire.connect_to(phil, :spouse)
43
+ claire.connect_to(haley, :child)
44
+ claire.connect_to(alex, :child)
45
+ claire.connect_to(luke, :child)
46
+
47
+ # Pritchett --------------------------------------------------------------
48
+
49
+ # Jay
50
+ jay.connect_to(gloria, :spouse)
51
+ jay.connect_to(claire, :child)
52
+ jay.connect_to(mitchell, :child)
53
+ jay.connect_to(unnamed, :child)
54
+ jay.connect_to(dede, :divorced)
55
+
56
+ # Gloria
57
+ gloria.connect_to(jay, :spouse)
58
+ gloria.connect_to(manny, :child)
59
+ gloria.connect_to(unnamed, :child)
60
+ gloria.connect_to(javier, :divorced)
61
+
62
+ # Tucker-Pritchett -------------------------------------------------------
63
+
64
+ mitchell.connect_to(cameron, :spouse)
65
+ mitchell.connect_to(lily, :child)
66
+
67
+ cameron.connect_to(mitchell, :spouse)
68
+ cameron.connect_to(lily, :child)
69
+
70
+ # Others -----------------------------------------------------------------
71
+
72
+ dede.connect_to(claire, :child)
73
+ dede.connect_to(mitchell, :child)
74
+
75
+ javier.connect_to(manny, :child)
76
+
77
+ frank.connect_to(phil, :child)
78
+ frank.connect_to(sarah, :spouse)
79
+
80
+ sarah.connect_to(phil, :child)
81
+ sarah.connect_to(frank, :spouse)
82
+
83
+ graph
84
+ end
85
+ end
86
+
87
+ # Find out that Manny is Jay's step-child with:
88
+ #
89
+ # jay = Turbine.stub.node(:jay)
90
+ # jay.out(:spouse).out(:child) - jay.out(:child)
91
+ #
92
+ # # => #<Turbine::Collection {#<Turbine::Node key=:manny>}>
93
+ #
94
+ # ... or that Alex has two siblings:
95
+ #
96
+ # alex = Turbine.stub.node(:alex)
97
+ # alex.in(:child).out(:child) - [alex]
98
+ #
99
+ # ... or that Jay and Gloria have a single "common" child:
100
+ #
101
+ # graph = Turbine.stub
102
+ # jay, gloria = graph.node(:jay), graph.node(:gloria)
103
+ #
104
+ # jay.out(:child) & gloria.out(:child)
105
+ #
106
+ # # => #<Turbine::Collection {#<Turbine::Node key=:unnamed>}>
107
+ #
108
+ # ... or who are Luke's uncles and aunts:
109
+ #
110
+ # graph = Turbine.stub
111
+ # luke = graph.node(:luke)
112
+ #
113
+ # parents = luke.in(:child)
114
+ # grandparents = parents.in(:child)
115
+ #
116
+ # # Remove Claire, Luke's mother:
117
+ # uncles_and_aunts = grandparents.out(:child) - parents
118
+ #
119
+ # # And add the uncle and aunt spouses for the full list.
120
+ # uncles_and_aunts + uncles_and_aunts.out(:spouse)
121
+ #
122
+ # # => #<Turbine::Collection {
123
+ # #<Turbine::Node key=:mitchell>,
124
+ # #<Turbine::Node key=:unnamed>,
125
+ # #<Turbine::Node key=:cameron>}>
data/lib/turbine.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'tsort'
2
+ require 'set'
3
+ require 'forwardable'
4
+
5
+ # On with the library...
6
+ require 'turbine/properties'
7
+ require 'turbine/algorithms/tarjan'
8
+ require 'turbine/algorithms/filtered_tarjan'
9
+ require 'turbine/edge'
10
+ require 'turbine/errors'
11
+ require 'turbine/graph'
12
+ require 'turbine/pipeline/dsl'
13
+ require 'turbine/pipeline/segment'
14
+ require 'turbine/pipeline/trace'
15
+ require 'turbine/pipeline/expander'
16
+ require 'turbine/pipeline/filter'
17
+ require 'turbine/pipeline/journal'
18
+ require 'turbine/pipeline/journal_filter'
19
+ require 'turbine/pipeline/pump'
20
+ require 'turbine/pipeline/transform'
21
+ require 'turbine/pipeline/traversal'
22
+ require 'turbine/pipeline/sender'
23
+ require 'turbine/pipeline/split'
24
+ require 'turbine/pipeline/unique'
25
+ require 'turbine/traversal/base'
26
+ require 'turbine/traversal/breadth_first'
27
+ require 'turbine/traversal/depth_first'
28
+ require 'turbine/node'
29
+ require 'turbine/version'
@@ -0,0 +1,33 @@
1
+ module Turbine
2
+ module Algorithms
3
+ # Internal: A wrapper around the Ruby stdlib implementation of Tarjan's
4
+ # strongly connected components and topological sort algorithms. Restricts
5
+ # the sort to only those edges which match the filter.
6
+ class FilteredTarjan < Tarjan
7
+
8
+ # Public: Creates a FilteredTarjan instance. If you simply wish to
9
+ # filter by the edge label, the standard Tarjan class will do this with
10
+ # better performance.
11
+ #
12
+ # graph - A Turbine graph whose nodes are to be sorted.
13
+ # &filter - Block which sleects the edges used to perform the sort.
14
+ #
15
+ # Returns a FilteredTarjan.
16
+ def initialize(graph, &filter)
17
+ super(graph, nil)
18
+ @filter = filter
19
+ end
20
+
21
+ #######
22
+ private
23
+ #######
24
+
25
+ # Internal: Used by TSort to iterate through each +out+ node.
26
+ #
27
+ # Returns nothing.
28
+ def tsort_each_child(node)
29
+ node.out_edges.select(&@filter).each { |edge| yield edge.to }
30
+ end
31
+ end # FilteredTarjan
32
+ end # Algorithms
33
+ end # Turbine
@@ -0,0 +1,50 @@
1
+ module Turbine
2
+ module Algorithms
3
+ # Internal: A wrapper around the Ruby stdlib implementation of Tarjan's
4
+ # strongly connected components and topological sort algorithms.
5
+ class Tarjan
6
+ include TSort
7
+
8
+ # Public: Creates a new Tarjan instance used for topologically sorting
9
+ # graphs.
10
+ #
11
+ # graph - A Turbine graph whose nodes are to be sorted.
12
+ # label - An optional label which will be used when traversing from a
13
+ # node to its +out+ nodes.
14
+ #
15
+ # For example
16
+ #
17
+ # Turbine::Algorithms::Tarjan.new(graph).tsort
18
+ # # => [ [ #<Node key=one>, #<Node key=two>, #<Node key=three> ],
19
+ # [ #<Node key=five> ],
20
+ # [ #<Node key=six>, #<Node key=seven> ] ]
21
+ #
22
+ # Turbine::Algorithms::Tarjan.new(graph, :spouse).tsort
23
+ # # => [ ... ]
24
+ #
25
+ # Returns a Tarjan.
26
+ def initialize(graph, label = nil)
27
+ @nodes = graph.nodes
28
+ @label = label
29
+ end
30
+
31
+ #######
32
+ private
33
+ #######
34
+
35
+ # Internal: Used by TSort to iterate through each node in the graph.
36
+ #
37
+ # Returns nothing.
38
+ def tsort_each_node
39
+ @nodes.each { |node| yield node }
40
+ end
41
+
42
+ # Internal: Used by TSort to iterate through each +out+ node.
43
+ #
44
+ # Returns nothing.
45
+ def tsort_each_child(node)
46
+ node.nodes(:out, @label).each { |out| yield out }
47
+ end
48
+ end # Tarjan
49
+ end # Algorithms
50
+ end # Turbine
@@ -0,0 +1,94 @@
1
+ module Turbine
2
+ # Edges represent a connection between an +from+ node and an +to+ node.
3
+ #
4
+ # Note that simply creating an Edge does not actually establish the
5
+ # connection between the two nodes. Rather than creating the Edge
6
+ # instance manually, Node can do this for you with +Node#connect_to+ and
7
+ # +Node#connect_via+:
8
+ #
9
+ # jay = Turbine::Node.new(:jay)
10
+ # gloria = Turbine::Node.new(:gloria)
11
+ #
12
+ # jay.connect_to(gloria, :spouse)
13
+ # gloria.connect_to(jay, :spouse)
14
+ #
15
+ # However, if you want to do it manually (perhaps with a subclass of Edge),
16
+ # you should use +Node#connect_via+:
17
+ #
18
+ # jay = Turbine::Node.new(:jay)
19
+ # gloria = Turbine::Node.new(:gloria)
20
+ #
21
+ # jay_married_to_gloria = Turbine::Edge.new(jay, gloria, :spouse)
22
+ # gloria_married_to_jay = Turbine::Edge.new(gloria, jay, :spouse)
23
+ #
24
+ # jay.connect_via(jay_married_to_gloria)
25
+ # gloria.connect_via(jay_married_to_gloria)
26
+ #
27
+ # jay.connect_via(gloria_married_to_jay)
28
+ # gloria.connect_via(gloria_married_to_jay)
29
+ #
30
+ class Edge
31
+ include Properties
32
+
33
+ # Public: The node which the edge leaves; the edge is an +out_edge+ on
34
+ # this node.
35
+ attr_reader :from
36
+
37
+ # Public: The node to which the edge points; the edge is an +in_edge+ on
38
+ # this node.
39
+ attr_reader :to
40
+
41
+ # Public: Returns the optional label assigned to the edge.
42
+ attr_reader :label
43
+
44
+ # Attribute aliases.
45
+ alias_method :parent, :from
46
+ alias_method :child, :to
47
+
48
+ # Public: Creates a new Edge.
49
+ #
50
+ # from_node - The Node from which the edge originates.
51
+ # in_node - The Node to which the edge points.
52
+ # label - An optional label for describing the nature of the
53
+ # relationship between the two nodes.
54
+ # properties - Optional key/value properties to be associated with the
55
+ # edge.
56
+ #
57
+ def initialize(from_node, to_node, label = nil, properties = nil)
58
+ @from = from_node
59
+ @to = to_node
60
+ @label = label
61
+
62
+ self.properties = properties
63
+ end
64
+
65
+ # Public: Determines if the +other+ edge is similar to this one.
66
+ #
67
+ # Two edges are considered similar if have the same +to+ and +from+ nodes,
68
+ # and their label is identical.
69
+ #
70
+ # Returns true or false.
71
+ def similar?(other)
72
+ other && other.to == @to && other.from == @from && other.label == @label
73
+ end
74
+
75
+ # Public: Returns a human-readable version of the edge.
76
+ def inspect
77
+ "#<#{ self.class.name } #{ to_s }>".sub('>', "\u27A4")
78
+ end
79
+
80
+ # Public: Returns a human-readable version of the edge by showing the from
81
+ # and to nodes, connected by the label.
82
+ def to_s
83
+ "#{ @from.key.inspect } -#{ @label.inspect }-> #{ @to.key.inspect }"
84
+ end
85
+
86
+ # Internal: A low-level method which retrieves the node in a given
87
+ # direction. Used for compatibility with Pipeline.
88
+ #
89
+ # Returns a Node.
90
+ def nodes(direction, *)
91
+ direction == :to ? @to : @from
92
+ end
93
+ end # Edge
94
+ end # Turbine
@@ -0,0 +1,74 @@
1
+ module Turbine
2
+ # Error class which serves as a base for all errors which occur in Turbine.
3
+ TurbineError = Class.new(StandardError)
4
+
5
+ # Internal: Creates a new error class which inherits from TurbineError,
6
+ # whose message is created by evaluating the block you give.
7
+ #
8
+ # For example
9
+ #
10
+ # MyError = error_class do |weight, limit|
11
+ # "#{ weight } exceeds #{ limit }"
12
+ # end
13
+ #
14
+ # raise MyError.new(5000, 2500)
15
+ # # => #<Turbine::MyError: 5000 exceeds 2500>
16
+ #
17
+ # Returns an exception class.
18
+ def self.error_class(superclass = TurbineError, &block)
19
+ Class.new(superclass) do
20
+ def initialize(*args) ; super(make_message(*args)) ; end
21
+ define_method(:make_message, &block)
22
+ end
23
+ end
24
+
25
+ # Added a node to a graph, when one already exists with the same key.
26
+ DuplicateNodeError = error_class do |key|
27
+ "Graph already has a node with the key #{ key.inspect }"
28
+ end
29
+
30
+ # Attempted an operation on a Node which does not exist.
31
+ NoSuchNodeError = error_class do |key|
32
+ "Graph does not contain a node with the key #{ key.inspect }"
33
+ end
34
+
35
+ # Added an edge between two nodes which was too similar to an existing edge.
36
+ # See Edge#similar?
37
+ DuplicateEdgeError = error_class do |node, edge|
38
+ "Another edge already exists on #{ node.inspect } which is too similar " \
39
+ "to #{ edge.inspect }"
40
+ end
41
+
42
+ # Attempted to set properties on an object, when the properties were not in
43
+ # the form of a hash, or were otherwise invalid in some way.
44
+ InvalidPropertiesError = error_class do |model, properties|
45
+ "Tried to assign properties #{ properties.inspect } on " \
46
+ "#{ model.inspect } - it must be a Hash, or subclass of Hash"
47
+ end
48
+
49
+ # Tried to access values from a non-existant Journal segment.
50
+ NoSuchJournalError = error_class do |name|
51
+ "No such upstream journal: #{ name.inspect }"
52
+ end
53
+
54
+ # Attempted to get trace information from a pipeline when tracing has not
55
+ # been enabled.
56
+ TracingNotEnabledError = error_class do |segment|
57
+ "You cannot get trace information from the #{ segment.inspect } " \
58
+ "segment as tracing is not enabled"
59
+ end
60
+
61
+ # Tried to enable tracing on a segment which doesn't support it.
62
+ NotTraceableError = error_class do |segment|
63
+ "You cannot enable tracing on pipelines with #{ segment.class.name } " \
64
+ "segments"
65
+ end
66
+
67
+ # Tried to topologically sort a graph which contains loops.
68
+ class CyclicError < TurbineError
69
+ def initialize(orig_exception)
70
+ set_backtrace(orig_exception.backtrace)
71
+ super(orig_exception.message)
72
+ end
73
+ end
74
+ end # Turbine
@@ -0,0 +1,113 @@
1
+ module Turbine
2
+ # Contains nodes and edges.
3
+ class Graph
4
+
5
+ # Public: Creates a new graph.
6
+ def initialize
7
+ @nodes = {}
8
+ end
9
+
10
+ # Public: Adds the +node+ to the graph.
11
+ #
12
+ # node - The node to be added.
13
+ #
14
+ # Raises a DuplicateNodeError if the graph already contains a node with
15
+ # the same key.
16
+ #
17
+ # Returns the node.
18
+ def add(node)
19
+ if @nodes.key?(node.key)
20
+ raise DuplicateNodeError.new(node.key)
21
+ end
22
+
23
+ @nodes[node.key] = node
24
+ end
25
+
26
+ # Public: Removes the +node+ from the graph and disconnects any nodes
27
+ # which have edges to the +node+.
28
+ #
29
+ # node - The node to be deleted.
30
+ #
31
+ # Raises a NoSuchNodeError if the graph does not contain the given +node+.
32
+ #
33
+ # Returns the node.
34
+ def delete(node)
35
+ unless @nodes.key?(node.key)
36
+ raise NoSuchNodeError.new(node.key)
37
+ end
38
+
39
+ (node.edges(:out) + node.edges(:in)).each do |edge|
40
+ edge.from.disconnect_via(edge)
41
+ edge.to.disconnect_via(edge)
42
+ end
43
+
44
+ @nodes.delete(node.key)
45
+ end
46
+
47
+ # Public: Retrieves the node whose key is +key+.
48
+ #
49
+ # key - The key of the desired node.
50
+ #
51
+ # Returns the node, or nil if no such node is known.
52
+ def node(key)
53
+ @nodes[key]
54
+ end
55
+
56
+ # Public: All of the nodes in an array.
57
+ #
58
+ # Generally speaking, the nodes will be returned in the same order as
59
+ # they were added to the graph, however this may very depending on your
60
+ # Ruby implementation.
61
+ #
62
+ # Returns an array of nodes.
63
+ def nodes
64
+ @nodes.values
65
+ end
66
+
67
+ # Public: Topologically sorts the nodes in the graph so that nodes with
68
+ # no in edges appear at the beginning of the array, and those deeper
69
+ # within the graph are at the end.
70
+ #
71
+ # label - An optional label used to limit the edges used when traversing
72
+ # from one node to its outward nodes.
73
+ #
74
+ # Raises CyclicError if the graph contains loops.
75
+ #
76
+ # Returns an array.
77
+ def tsort(label = nil)
78
+ Algorithms::Tarjan.new(self, label).tsort
79
+ rescue TSort::Cyclic => exception
80
+ raise CyclicError.new(exception)
81
+ end
82
+
83
+ # Public: Uses Tarjan's strongly-connected components algorithm to detect
84
+ # nodes which are interrelated.
85
+ #
86
+ # label - An optional label used to limit the edges used when traversing
87
+ # from one node to its outward nodes.
88
+ #
89
+ # For example
90
+ #
91
+ # graph.strongly_connected_components
92
+ # # => [ [ #<Node key=one> ],
93
+ # [ #<Node key=two>, #<Node key=three>, #<Node key=four> ],
94
+ # [ #<Node key=five>, #<Node key=six ] ]
95
+ #
96
+ # Returns an array.
97
+ def strongly_connected_components(label = nil)
98
+ Algorithms::Tarjan.new(self, label).strongly_connected_components
99
+ end
100
+
101
+ # Public: A human-readable version of the graph.
102
+ #
103
+ # Returns a string.
104
+ def inspect
105
+ edge_count = @nodes.values.each_with_object(Set.new) do |node, edges|
106
+ edges.merge(node.edges(:out))
107
+ end.length
108
+
109
+ "#<#{self.class} (#{ @nodes.length } nodes, #{ edge_count } edges)>"
110
+ end
111
+
112
+ end # Graph
113
+ end # Turbine