turbine-graph 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,246 @@
1
+ module Turbine
2
+ # In graph theory, a node is the fundamental unit of which graphs are
3
+ # formed: a directed graph consists of a set of nodes and a set of edges.
4
+ class Node
5
+ include Properties
6
+
7
+ # Public: Returns the unique key which identifies the node.
8
+ attr_reader :key
9
+
10
+ # Creates a new Node.
11
+ #
12
+ # key - A unique identifier for the node. The uniqueness of the key
13
+ # is not checked upon initializing, but instead when the node
14
+ # is added to the graph (Graph#add).
15
+ # properties - Optional key/value properties to be associated with the
16
+ # node.
17
+ #
18
+ # Returns the node.
19
+ def initialize(key, properties = nil)
20
+ @key = key
21
+ @in_edges = Set.new
22
+ @out_edges = Set.new
23
+
24
+ self.properties = properties
25
+ end
26
+
27
+ # Public: Returns nodes which have an outgoing edge to this node.
28
+ #
29
+ # label - An optional label by which to filter the in edges, before
30
+ # fetching the matched nodes.
31
+ #
32
+ # Returns an array of Node instances.
33
+ def in(label = nil)
34
+ Pipeline.dsl(self).in(label)
35
+ end
36
+
37
+ # Public: Returns verticies to which this node has outgoing edges.
38
+ #
39
+ # label - An optional label by which to filter the out edges, before
40
+ # fetching the matched nodes.
41
+ #
42
+ # Returns an array of Node instances.
43
+ def out(label = nil)
44
+ Pipeline.dsl(self).out(label)
45
+ end
46
+
47
+ # Public: Returns this node's incoming edges.
48
+ #
49
+ # label - An optional label; only edges with this label will be returned.
50
+ # Passing nil will return all in edges.
51
+ #
52
+ # Raises an InvalidEdgeFilterError if you supply both a +label+ and
53
+ # +block+ for filtering the edges.
54
+ #
55
+ # Returns an array of Edges.
56
+ def in_edges(label = nil)
57
+ Pipeline.dsl(self).in_edges(label)
58
+ end
59
+
60
+ # Public: Returns this node's outgoing edges.
61
+ #
62
+ # label - An optional label; only edges with this label will be returned.
63
+ # Passing nil will return all out edges.
64
+ #
65
+ # Raises an InvalidEdgeFilterError if you supply both a +label+ and
66
+ # +block+ for filtering the edges.
67
+ #
68
+ # Returns an array of Edges.
69
+ def out_edges(label = nil)
70
+ Pipeline.dsl(self).out_edges(label)
71
+ end
72
+
73
+ # Public: Returns an enumerator containing all nodes which are outward
74
+ # nodes, and all of their outward nodes.
75
+ #
76
+ # Uses a BreadthFirst traversal so that immediately adjacent nodes are
77
+ # visited before more distant nodes.
78
+ #
79
+ # Returns an Enumerator containing Nodes.
80
+ def descendants(label = nil)
81
+ Pipeline.dsl(self).traverse(:out, label)
82
+ end
83
+
84
+ # Public: Returns an enumerator containing all nodes which are inward
85
+ # nodes, and all of their inward nodes.
86
+ #
87
+ # Uses a BreadthFirst traversal so that immediately adjacent nodes are
88
+ # visited before more distant nodes.
89
+ #
90
+ # Returns an Enumerator containing Nodes.
91
+ def ancestors(label = nil)
92
+ Pipeline.dsl(self).traverse(:in, label)
93
+ end
94
+
95
+ # Internal: Low-level method which retrieves all of the edges in a given
96
+ # +direction+, in an array.
97
+ #
98
+ # Returns an array of edges.
99
+ def edges(direction, label = nil)
100
+ select_edges(direction == :in ? @in_edges : @out_edges, label)
101
+ end
102
+
103
+ # Internal: Low-level method which retrieves all of the nodes in a given
104
+ # +direction+, in an array.
105
+ #
106
+ # Returns an array of nodes.
107
+ def nodes(direction, label = nil)
108
+ edges(direction, label).map(&(direction == :in ? :from : :to))
109
+ end
110
+
111
+ # Public: Returns a human-readable version of the node.
112
+ def inspect
113
+ "#<#{ self.class.name } key=#{ @key.inspect }>"
114
+ end
115
+
116
+ alias_method :to_s, :inspect
117
+
118
+ # Public: Connects this node to another.
119
+ #
120
+ # target - The node to which you want to connect. The +target+ node
121
+ # will be the "from" end of the edge.
122
+ # label - An optional label describing the relationship between the
123
+ # two nodes.
124
+ # properties - Optional key/value properties to be associated with the
125
+ # edge.
126
+ #
127
+ # Example:
128
+ #
129
+ # phil = Turbine::Node.new(:phil)
130
+ # luke = Turbine::Node.new(:luke)
131
+ #
132
+ # phil.connect_to(luke, :child)
133
+ #
134
+ # Returns the Edge which was created.
135
+ #
136
+ # Raises a Turbine::DuplicateEdgeError if the Edge already existed.
137
+ def connect_to(target, label = nil, properties = nil)
138
+ Edge.new(self, target, label, properties).tap do |edge|
139
+ self.connect_via(edge)
140
+ target.connect_via(edge)
141
+ end
142
+ end
143
+
144
+ # Public: Disconnects this node from the +target+. Assumes that the
145
+ # receiver is the +from+ node.
146
+ #
147
+ # target - The node from which the receiver is to be disconnected.
148
+ # label - An optional label; only the edge with this label will be
149
+ # removed, with other edges kept intact. No label will remove all
150
+ # outward edges between the receiver and the target.
151
+ #
152
+ # Raises NoSuchEdge if the two nodes are not connected.
153
+ #
154
+ # Returns nothing.
155
+ def disconnect_from(target, label = nil)
156
+ edges(:out, label).select { |edge| edge.to == target }.each do |edge|
157
+ disconnect_via(edge)
158
+ target.disconnect_via(edge)
159
+ end
160
+
161
+ nil
162
+ end
163
+
164
+ # Internal: Given an Edge, establishes the connection for this node.
165
+ #
166
+ # Please note that you need to call +connect_via+ on both the "from" and
167
+ # "to" nodes. Unless you need to create the connection using a subclass of
168
+ # Edge, you will likey prefer using the simpler +connect_to+.
169
+ #
170
+ # Example:
171
+ #
172
+ # phil = Turbine::Node.new(:phil)
173
+ # haley = Turbine::Node.new(:haley)
174
+ #
175
+ # edge = Turbine::Edge.new(phil, haley, :child)
176
+ #
177
+ # # Adds an link from "phil" to "haley".
178
+ # phil.connect_via(edge)
179
+ # haley.connect_via(edge)
180
+ #
181
+ # Raises a Turbine::CannotConnectError if this node is not the +from+ or
182
+ # +to+ node specified by the edge.
183
+ #
184
+ # Returns the given edge.
185
+ def connect_via(edge)
186
+ connect_endpoint(@in_edges, edge) if edge.to == self
187
+ connect_endpoint(@out_edges, edge) if edge.from == self
188
+
189
+ edge
190
+ end
191
+
192
+ # Internal: Given an edge, removes the connection for this node.
193
+ #
194
+ # Please note that you need to call +disconnect_via+ on both the "from"
195
+ # and "to" nodes.
196
+ #
197
+ # Example:
198
+ #
199
+ # haley = Turbine::Node.new(:haley)
200
+ # dylan = Turbine::Node.new(:dylan)
201
+ #
202
+ # edge = haley.connect_to(dylan, :boyfriend)
203
+ #
204
+ # haley.disconnect_via(edge)
205
+ # dylan.disconnect_via(edge)
206
+ #
207
+ # Returns nothing.
208
+ def disconnect_via(edge)
209
+ @in_edges.delete(edge)
210
+ @out_edges.delete(edge)
211
+
212
+ nil
213
+ end
214
+
215
+ #######
216
+ private
217
+ #######
218
+
219
+ # Internal: Given an edge, and a Node's in_edges or out_edges, adds the
220
+ # edge only if there is not a similar edge already present.
221
+ #
222
+ # collection - The collection to which the edge is to be added.
223
+ # edge - The edge.
224
+ #
225
+ # Returns nothing.
226
+ def connect_endpoint(collection, edge)
227
+ if collection.any? { |o| ! edge.equal?(o) && edge.similar?(o) }
228
+ raise DuplicateEdgeError.new(self, edge)
229
+ end
230
+
231
+ collection.add(edge)
232
+ end
233
+
234
+ # Internal: Given an array of edges, and an optional label label, selects
235
+ # the edges from the given set.
236
+ #
237
+ # edges - The array of edges to be filtered.
238
+ # label - The label of the edges to be emitted.
239
+ #
240
+ # Returns an array of edges.
241
+ def select_edges(edges, label)
242
+ label.nil? ? edges : edges.select { |edge| edge.label == label }
243
+ end
244
+
245
+ end # Node
246
+ end # Turbine
@@ -0,0 +1,31 @@
1
+ # Pipeline
2
+
3
+ Pipeline classes, appended to one another, provide a way to take a
4
+ source and lazily transform and filter that source. It is also more
5
+ efficient than the current `in` and `out` implementations in that
6
+ chaining multiple expressions together can be done without the need for
7
+ temporary collections between each chained method, and each source is
8
+ traversed only once.
9
+
10
+ This provides support for interesting traversal expressions:
11
+
12
+ ```Ruby
13
+ # Get and "name" the child's parents:
14
+ luke.in(:child).as('parents').
15
+ # Get Grandparents:
16
+ in(:child).
17
+ # Get Grandparents' children (which includes Luke's parents):
18
+ out(:child).uniq.
19
+ # Remove Luke's parents, leaving the Uncles and Aunts:
20
+ except('parents').
21
+ # Add the Uncles' and Aunts' spouses:
22
+ also { |node| node.out(:spouse) }
23
+ ```
24
+
25
+ Pipeline is inspired by Dave Thomas' 2008 articles on using Fiber to
26
+ create pipelines in Ruby<sup>[1][pipelines-one] & [2][pipelines-two]</sup>,
27
+ and the Java ["Pipes" library][pipes-library].
28
+
29
+ [pipelines-one]: http://pragdave.blogs.pragprog.com/pragdave/2007/12/pipelines-using.html
30
+ [pipelines-two]: http://pragdave.blogs.pragprog.com/pragdave/2008/01/pipelines-using.html
31
+ [pipes-library]: https://github.com/tinkerpop/pipes/wiki
@@ -0,0 +1,275 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # Public: Starts a new Pipeline chain using the given +source+ as the
4
+ # source.
5
+ #
6
+ # source - An object, or array of objects, which will be iterated through
7
+ # the pipeline.
8
+ #
9
+ # Returns a DSL.
10
+ def self.dsl(source)
11
+ DSL.new(Pump.new(Array(source)))
12
+ end
13
+
14
+ # Provides the chaining DSL used throughout Turbine, such as when calling
15
+ # Node#in, Node#descendants, etc.
16
+ class DSL
17
+ extend Forwardable
18
+ include Enumerable
19
+
20
+ def_delegators :@source, :to_a, :each, :to_s
21
+
22
+ # The final segment in the pipeline.
23
+ attr_reader :source
24
+
25
+ # Public: Creates a new DSL instance.
26
+ #
27
+ # source - A Segment which acts as the head of the pipeline. Normally
28
+ # an instance of Pump.
29
+ #
30
+ # Returns a DSL.
31
+ def initialize(source)
32
+ @source = source
33
+ end
34
+
35
+ # Public: Queries each input for its +key+ property. Expects the input
36
+ # to include Turbine::Properties.
37
+ #
38
+ # key - The property key to be queried.
39
+ #
40
+ # For example
41
+ #
42
+ # pipe.get(:age) # => [11, 15, 18, 44, 46]
43
+ #
44
+ # Returns a new DSL.
45
+ def get(key)
46
+ append(Sender.new(:get, key))
47
+ end
48
+
49
+ # Public: Retrieves the in_edges from the input nodes.
50
+ #
51
+ # label - An optional label; only edges with a matching label will be
52
+ # emitted by the pipeline.
53
+ #
54
+ # Returns a DSL.
55
+ def in_edges(label = nil)
56
+ append(Sender.new(:edges, :in, label))
57
+ end
58
+
59
+ # Public: Retrieves the out_edges from the input nodes.
60
+ #
61
+ # label - An optional label; only edges with a matching label will be
62
+ # emitted by the pipeline.
63
+ #
64
+ # Returns a new DSL.
65
+ def out_edges(label = nil)
66
+ append(Sender.new(:edges, :out, label))
67
+ end
68
+
69
+ # Public: Retrieves the inbound nodes on the input node or edge.
70
+ #
71
+ # label - An optional label; only edges connected to the node via an
72
+ # edge with this label will be emitted by the pipeline.
73
+ #
74
+ # Returns a new DSL.
75
+ def in(label = nil)
76
+ append(Sender.new(:nodes, :in, label))
77
+ end
78
+
79
+ # Public: Retrieves the outbound nodes on the input node or edge.
80
+ #
81
+ # label - An optional label; only edges connected to the node via an
82
+ # edge with this label will be emitted by the pipeline.
83
+ #
84
+ # Returns a new DSL.
85
+ def out(label = nil)
86
+ append(Sender.new(:nodes, :out, label))
87
+ end
88
+
89
+ # Public: Using the breadth-first traversal strategy, fetches all of a
90
+ # node's adjacent nodes, and their adjacent nodes, and so on, in a given
91
+ # +direction+
92
+ #
93
+ # direction - In which direction from the current node do you wat to
94
+ # traverse? :in or :out?
95
+ # label - An optional label which is used to restrict the edges
96
+ # traversed to those with the label.
97
+ #
98
+ # For example
99
+ #
100
+ # # Fetches all nodes via outgoing edges.
101
+ # dsl.traverse(:out).to_a
102
+ #
103
+ # # Gets all out nodes via edges which have the :child label.
104
+ # dsl.traverse(:out, :child)
105
+ #
106
+ # Returns a new DSL.
107
+ def traverse(direction, label = nil)
108
+ append(Traverse.new(direction, label))
109
+ end
110
+
111
+ # Public: Given a block, emits input elements for which the block
112
+ # evaluates to true.
113
+ #
114
+ # block - A block used to determine which element are emitted.
115
+ #
116
+ # Returns a new DSL.
117
+ def select(&block)
118
+ append(Filter.new(&block))
119
+ end
120
+
121
+ # Public: Given a block, emits input elements for which the block
122
+ # evaluates to false.
123
+ #
124
+ # block - A block used to determine which elements are emitted.
125
+ #
126
+ # Returns a new DSL.
127
+ def reject(&block)
128
+ append(Filter.new { |value| ! block.call(value) })
129
+ end
130
+
131
+ # Public: Given a block, transforms each input value to the result of
132
+ # running the block with the input.
133
+ #
134
+ # block - A block used to transform each input value.
135
+ #
136
+ # Returns a new DSL.
137
+ def map(&block)
138
+ append(Transform.new(&block))
139
+ end
140
+
141
+ # Public: Splits the pipeline into separate branches, computes the
142
+ # values from each branch in turn, then combines the results.
143
+ #
144
+ # branches - One or more blocks which will be given the DSL.
145
+ #
146
+ # For example
147
+ #
148
+ # nodes.split(->(x) { x.get(:gender) },
149
+ # ->(x) { x.in_edges.length },
150
+ # ->(x) { x.out_edges.length }).to_a
151
+ # # => [ :male, 2, 3, :female, 1, 6, :female, 2, 2, ... ]
152
+ #
153
+ # Returns a new DSL.
154
+ def split(*branches)
155
+ append(Split.new(*branches))
156
+ end
157
+
158
+ # Public: Like +split+, but also yields the input value before running
159
+ # each branch.
160
+ #
161
+ # branches - One or more blocks which will be given the DSL.
162
+ #
163
+ # For example
164
+ #
165
+ # # Yields each node, and their outwards nodes connected with a
166
+ # # :spouse edge.
167
+ # nodes.also(->(node) { node.out(:spouse) })
168
+ #
169
+ # If you only want to supply a single branch, you can pass a block
170
+ # instead.
171
+ #
172
+ # nodes.also { |node| node.out(:child) }
173
+ #
174
+ # Returns a new DSL.
175
+ def also(*branches, &block)
176
+ append(Also.new(*branches, &block))
177
+ end
178
+
179
+ # Public: Captures all of the values emitted by the previous segment so
180
+ # that a later segment (e.g. "only" or "except") can use them.
181
+ #
182
+ # name - A name assigned to the captured values.
183
+ #
184
+ # Returns a new DSL.
185
+ def as(name)
186
+ append(Journal.new(name))
187
+ end
188
+
189
+ # Public: Creates a filter so that only values which were present in a
190
+ # named journal (created using "as") are emitted.
191
+ #
192
+ # journal_name - The name of the "as" journal.
193
+ #
194
+ # For example
195
+ #
196
+ # # Did your grandparents "friend" your parents?
197
+ # node.in(:child).as(:parents).in(:child).out(:friend).only(:parents)
198
+ #
199
+ # Returns a new DSL.
200
+ def only(journal_name)
201
+ append(JournalFilter.new(:only, journal_name))
202
+ end
203
+
204
+ # Public: Creates a filter so that only values which were not present in
205
+ # a named journal (created using "as") are emitted.
206
+ #
207
+ # name - The name of the "as" journal.
208
+ #
209
+ # # Who are your uncles and aunts?
210
+ # node.in(:child).as(:parents).in(:child).out(:child).except(:parents)
211
+ #
212
+ # Returns a new DSL.
213
+ def except(journal_name)
214
+ append(JournalFilter.new(:except, journal_name))
215
+ end
216
+
217
+ # Public: Mutates the pipeline so that instead of returning a single
218
+ # value, it returns an array where each element is the result returned
219
+ # by each segment in the pipeline.
220
+ #
221
+ # Does not work correctly with pipelines where +descendants+ or
222
+ # +ancestors+ is used before +trace+.
223
+ #
224
+ # For example
225
+ #
226
+ # jay.out(:child).out(:child).trace.to_a
227
+ # # => [ [ #<Node key=:jay>, #<Node key=:claire>, #<Node key=:haley> ],
228
+ # # [ #<Node key=:jay>, #<Node key=:claire>, #<Node key=:alex> ],
229
+ # # ... ]
230
+ #
231
+ # This can be especially useful if you explicitly include edges in your
232
+ # pipeline:
233
+ #
234
+ # jay.out_edges(:child).in.out_edges(:child).in.trace.next
235
+ # # => [ [ #<Node key=:jay>,
236
+ # # #<Edge :jay -:child-> :claire>,
237
+ # # #<Node key=:claire>,
238
+ # # #<Edge :claire -:child-> :haley>,
239
+ # # #<Node key=:haley> ],
240
+ # # ... ]
241
+ #
242
+ # Returns a new DSL.
243
+ def trace
244
+ DSL.new(@source.append(Trace.new))
245
+ end
246
+
247
+ # Public: Filters each value so that only unique elements are emitted.
248
+ #
249
+ # block - An optional block used when determining if the value is
250
+ # unique. See Pipeline::Unique#initialize.
251
+ #
252
+ # Returns a new DSL.
253
+ def uniq(&block)
254
+ append(Unique.new(&block))
255
+ end
256
+
257
+ # Public: Creates a new DSL by appending the given +downstream+ segment
258
+ # to the current source.
259
+ #
260
+ # downstream - The segment to be added in the new DSL.
261
+ #
262
+ # Returns a new DSL.
263
+ def append(downstream)
264
+ DSL.new(@source.append(downstream))
265
+ end
266
+
267
+ # Public: A human-readable version of the DSL.
268
+ #
269
+ # Return a String.
270
+ def inspect
271
+ "#<#{ self.class.inspect } {#{ to_s }}>"
272
+ end
273
+ end # DSL
274
+ end # Pipeline
275
+ end # Turbine