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,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