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,51 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # A segment which transforms its input by sending a +message+ to each
4
+ # input and returning the result.
5
+ #
6
+ # ( Pump.new([1, 2]) | Sender.new(:to_s) ).to_a
7
+ # # => ['1', '2']
8
+ #
9
+ # Each item coming from the source segment must have a public method with
10
+ # the same name as the +message+.
11
+ #
12
+ # Some methods in Turbine return an Array, Collection, or Enumerator as a
13
+ # sort of "result set" -- such as Node#in, Node#descendants, etc. In these
14
+ # cases, each element in the result set is yielded separately before
15
+ # continuing with the next input. See Expander for more details.
16
+ class Sender < Expander
17
+ attr_reader :message, :args
18
+
19
+ # Public: Creates a new Sender segment.
20
+ #
21
+ # message - The message (method name) to be sent to each value in the
22
+ # pipeline.
23
+ # args - Optional arguments to be sent with the message.
24
+ #
25
+ # Returns a Sender.
26
+ def initialize(message, *args)
27
+ @message = message
28
+ @args = args
29
+
30
+ super()
31
+ end
32
+
33
+ # Public: Describes the segments through which each input will pass.
34
+ #
35
+ # Returns a string.
36
+ def to_s
37
+ "#{ source_to_s } | #{ message.to_s }" \
38
+ "(#{ args.map(&:inspect).join(', ') })"
39
+ end
40
+
41
+ #######
42
+ private
43
+ #######
44
+
45
+ def input
46
+ super.public_send(@message, *args)
47
+ end
48
+
49
+ end # Sender
50
+ end # Pipeline
51
+ end # Turbine
@@ -0,0 +1,132 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # Splits the upstream source into multiple pipelines which are evaluated
4
+ # in turn on the source, with the combined results being emitted.
5
+ #
6
+ # For example
7
+ #
8
+ # pump = Pump.new([node1, node2, node3])
9
+ # split = Split.new(->(x) { x.get(:age) },
10
+ # ->(x) { x.get(:gender) })
11
+ #
12
+ # (pump | split).to_a # => [ 18, :male, 27, :female, 25, :male ]
13
+ #
14
+ # You may supply as many separate branches as you wish.
15
+ class Split < Expander
16
+ Branch = Struct.new(:pump, :pipe)
17
+
18
+ # Public: Creates a new Split segment.
19
+ #
20
+ # branches - One or more procs; each proc is given a new pipeline DSL so
21
+ # that you may transform / filter the inputs before the
22
+ # results are merged back into the output.
23
+ #
24
+ # Returns a new Split.
25
+ def initialize(*branches)
26
+ if branches.none?
27
+ raise ArgumentError, 'Split requires at least one proc'
28
+ end
29
+
30
+ super()
31
+
32
+ # Each DSL is evaluated once, and +handle_result+ changes the source
33
+ # for each value being processed. This is more efficient than creating
34
+ # and evaluating a new DSL for every input.
35
+ @branches = branches.map do |branch|
36
+ dsl = Pipeline.dsl([])
37
+ pump = dsl.source
38
+
39
+ Branch.new(pump, branch.call(dsl))
40
+ end
41
+
42
+ # JRuby doesn't support calling +next+ on enum.cycle.with_index.
43
+ @branches_cycle = @branches.zip((0...@branches.length).to_a).cycle
44
+ end
45
+
46
+ # Public: Returns the trace containing the most recently emitted values
47
+ # for all source segments. The trace for the current branch pipeline is
48
+ # merged into the trace.
49
+ #
50
+ # See Segment#trace.
51
+ #
52
+ # Returns an array.
53
+ def trace
54
+ super { |trace| trace.push(*@previous_trace) }
55
+ end
56
+
57
+ # Public: Enables or disables tracing on the segment. Passes the boolean
58
+ # through to the internal branch pipelines also, so that their traces
59
+ # may be combined with the output.
60
+ #
61
+ # Returns the tracing setting.
62
+ def tracing=(use_tracing)
63
+ super
64
+
65
+ @branches.each do |branch|
66
+ branch.pipe.source.tracing = use_tracing
67
+ end
68
+ end
69
+
70
+ #######
71
+ private
72
+ #######
73
+
74
+ # Internal: Returns the next value to be processed by the pipeline.
75
+ #
76
+ # Calling +input+ will fetch the input from the upstream segment,
77
+ # process it on the first branch and return the value. The next call
78
+ # will process the same input on the second branch, and so on util the
79
+ # value has been passed through each branch. Only then do we fetch a new
80
+ # input and start over.
81
+ #
82
+ # Returns an object.
83
+ def input
84
+ branch, iteration = @branches_cycle.next
85
+
86
+ # We've been through each branch for the current source, time to fetch
87
+ # the next one?
88
+ if iteration.zero?
89
+ @branch_source = Array(super).to_enum
90
+ end
91
+
92
+ branch.pump.source = @branch_source
93
+ branch.pipe.source.rewind
94
+
95
+ values = branch.pipe.to_a
96
+
97
+ @previous_trace = branch.pipe.source.trace.drop(1) if @tracing
98
+
99
+ values.any? ? values : input
100
+ end
101
+ end # Split
102
+
103
+ # A special case of split which emits the input value, and the results
104
+ # of a the given branches.
105
+ #
106
+ # For example
107
+ #
108
+ # # Get your friends and their friends, and emit both as a single list.
109
+ # nodes.out(:friend).also(->(node) { node.out(:friend) })
110
+ #
111
+ class Also < Split
112
+ # Creates a new Also segment.
113
+ #
114
+ # branches - A single branch whose results will be emitted along with
115
+ # the input value.
116
+ #
117
+ # For example
118
+ #
119
+ # nodes.also(->(n) { n.out(:spouse) }, ->(n) { n.out(:child) })
120
+ #
121
+ # If you only need to supply a single branch, you can pass it as a block
122
+ # instead of a proc wrapped in an array.
123
+ #
124
+ # nodes.also { |n| n.out(:spouse) }
125
+ #
126
+ # Returns a new Also.
127
+ def initialize(*branches, &block)
128
+ super(*[->(node) { node }, *branches, block].compact)
129
+ end
130
+ end # Also
131
+ end # Pipeline
132
+ end # Turbine
@@ -0,0 +1,55 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # Trace alters the pipeline such that instead of returning a single
4
+ # "reduced" value each time the pipeline is run, an array is returned with
5
+ # each element containing the result of each segment.
6
+ #
7
+ # See DSL#trace for more information.
8
+ class Trace < Segment
9
+ # Public: Sets the segment which serves as the source for the Trace.
10
+ # Enables tracing on the source, and all of the parent sources.
11
+ #
12
+ # Returns the source.
13
+ def source=(upstream)
14
+ upstream.tracing = true
15
+ super
16
+ end
17
+
18
+ # Public: Runs the pipeline once, returning the full trace which was
19
+ # traversed in order to retrieve the value.
20
+ #
21
+ # Returns an object.
22
+ def next
23
+ @source.next
24
+ @source.trace
25
+ end
26
+
27
+ # When included into a segment, sets it so that the value emitted by the
28
+ # segment is not included in traces. Useful for filters which would
29
+ # otherwise result in a duplicate value in the trace.
30
+ module Transparent
31
+ # Public: Trace each transformation made to an input value.
32
+ #
33
+ # See Segment#trace.
34
+ #
35
+ # Returns an array.
36
+ def trace
37
+ @source.trace
38
+ end
39
+ end # Transparent
40
+
41
+ # When included into a segment, raises an error if the user tries to
42
+ # enable tracing.
43
+ module Untraceable
44
+ # Public: Enable or disable tracing on the segment. Raises a
45
+ # NotTraceableError when called with a truthy value.
46
+ #
47
+ # Returns the tracing setting.
48
+ def tracing=(use_tracing)
49
+ raise NotTraceableError.new(self) if use_tracing
50
+ super
51
+ end
52
+ end # Untraceable
53
+ end # Trace
54
+ end # Pipeline
55
+ end # Turbine
@@ -0,0 +1,49 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # A segment which transforms the input into something else. For example,
4
+ # a simple transform might receive an integer and output it's square root.
5
+ #
6
+ # Transform.new { |x| Math.sqrt(x) }
7
+ #
8
+ class Transform < Segment
9
+ # Public: Creates a new Transform element.
10
+ #
11
+ # You may opt to use the Transform class directly, passing a block when
12
+ # initializing which is used to transform each value into something
13
+ # else. Alternatively, provide no block and use a subclass with a custom
14
+ # +transform+ method.
15
+ #
16
+ # Without a filter block, all elements are emitted.
17
+ #
18
+ # block - An optional block used to transform each value passing through
19
+ # the pipeline into something else.
20
+ #
21
+ # Returns a transform.
22
+ def initialize(&block)
23
+ @transform = (block || method(:transform))
24
+ super()
25
+ end
26
+
27
+ #######
28
+ private
29
+ #######
30
+
31
+ # Internal: Handles each value from the pipeline, using the +transform+
32
+ # block or method to convert it into something else.
33
+ #
34
+ # value - The value being processed.
35
+ #
36
+ # Returns nothing.
37
+ def handle_value(value)
38
+ super(@transform.call(value))
39
+ end
40
+
41
+ # Internal: The default transform.
42
+ #
43
+ # Returns the +value+ untouched.
44
+ def transform(value)
45
+ value
46
+ end
47
+ end # Transform
48
+ end # Pipeline
49
+ end # Turbine
@@ -0,0 +1,34 @@
1
+ module Turbine
2
+ module Pipeline
3
+ class Traverse < Expander
4
+ include Trace::Untraceable
5
+
6
+ # Public: Creates a new Traverse segment. Uses one of the traversal
7
+ # classes to emit every descendant of the input node.
8
+ #
9
+ # direction - The direction in which to traverse edges. :in or :out.
10
+ # label - An optional label by which to restrict the edges
11
+ # traversed.
12
+ # klass - The traversal strategy. Defaults to BreadthFirst.
13
+ #
14
+ # Returns a new Traverse.
15
+ def initialize(direction, label = nil, klass = nil)
16
+ @direction = direction
17
+ @label = label
18
+ @klass ||= Traversal::BreadthFirst
19
+ end
20
+
21
+ #######
22
+ private
23
+ #######
24
+
25
+ # Public: Passes each value into a traversal class, emitting every
26
+ # adjacent node.
27
+ #
28
+ # Returns the traversed objects.
29
+ def input
30
+ @klass.new(super, @direction, [@label]).to_enum
31
+ end
32
+ end # Traverse
33
+ end # Pipeline
34
+ end # Turbine
@@ -0,0 +1,47 @@
1
+ module Turbine
2
+ module Pipeline
3
+ # A Pipeline segment which only emits values which it hasn't emitted
4
+ # previously.
5
+ #
6
+ # In order to determine if a value is a duplicate, Unique needs to keep a
7
+ # reference to each input it sees. For large result sets, you may prefer
8
+ # to sacrifice performance for reduced space complexity by passing a
9
+ # block; this used to reduce each input to a simpler value for storage and
10
+ # comparison:
11
+ #
12
+ # pipeline.uniq { |value| value.hash }
13
+ #
14
+ # See also: Array#uniq.
15
+ class Unique < Filter
16
+
17
+ # Public: Creates a new Unique segment.
18
+ #
19
+ # block - An optional block which is used to "reduce" each value for
20
+ # comparison with previously seen value.
21
+ #
22
+ # Returns a Unique.
23
+ def initialize(&block)
24
+ @seen = Set.new
25
+
26
+ super do |value|
27
+ key = block ? block.call(value) : value
28
+ seen = @seen.include?(key)
29
+
30
+ @seen.add(key)
31
+
32
+ not seen
33
+ end
34
+ end
35
+
36
+ # Public: Rewinds the segment so that iteration can happen from the
37
+ # first input again.
38
+ #
39
+ # Returns nothing.
40
+ def rewind
41
+ @seen.clear
42
+ super
43
+ end
44
+
45
+ end # Unique
46
+ end # Pipeline
47
+ end # Turbine
@@ -0,0 +1,48 @@
1
+ module Turbine
2
+ module Properties
3
+
4
+ # Public: Returns the properties associated with the model.
5
+ #
6
+ # Returns a hash containing the properties. This is the original
7
+ # properties hash, not a duplicate.
8
+ def properties
9
+ @properties ||= Hash.new
10
+ end
11
+
12
+ # Public: Mass-assigns properties to the model.
13
+ #
14
+ # new_props - A hash containing zero or more properties. The internal
15
+ # properties hash is set to whatever parameters you provide; a
16
+ # duplicate is not made before assignment. You may provide
17
+ # +nil+ to remove all properties.
18
+ #
19
+ # Returns the properties.
20
+ def properties=(new_props)
21
+ unless new_props.is_a?(Hash) || new_props.nil?
22
+ raise InvalidPropertiesError.new(self, new_props)
23
+ end
24
+
25
+ @properties = new_props
26
+ end
27
+
28
+ # Public: Sets a single property on the model.
29
+ #
30
+ # key - The property name.
31
+ # value - The value to be set.
32
+ #
33
+ # Returns the value.
34
+ def set(key, value)
35
+ properties[key] = value
36
+ end
37
+
38
+ # Public: Returns a single property on the model.
39
+ #
40
+ # key - The property to be retrieved.
41
+ #
42
+ # Returns the value or nil if the property does not exist.
43
+ def get(key)
44
+ properties[key]
45
+ end
46
+
47
+ end # Properties
48
+ end # Turbine
@@ -0,0 +1,133 @@
1
+ module Turbine
2
+ module Traversal
3
+ # Provides the means for traversing through the graph.
4
+ #
5
+ # Traversal classes do not themselves provide the methods commonly used
6
+ # for iterating through collections (each, map, etc), but act as a
7
+ # generator for the values in an Enumerator.
8
+ #
9
+ # enumerator = DepthFirst.new(node, :in).to_enum
10
+ # # => #<Enumerator: Node, Node, ...>
11
+ #
12
+ # enumerator.each { |node| ... }
13
+ # enumerator.map { |node| ... }
14
+ # # etc ...
15
+ #
16
+ # The Base class should not be used directly, but instead you should use
17
+ # DepthFirst or BreadthFirst which define strategies for the order in
18
+ # which items are traversed.
19
+ #
20
+ # Each unique item is traversed a maximum of once (loops are not
21
+ # repeatedly followed).
22
+ #
23
+ # Traversals are normally used to iterate through nodes, however you may
24
+ # also use them to traverse edges by providing a +fetcher+ argument which
25
+ # tells the traversal how to reach the next set of adjacent items:
26
+ #
27
+ # DepthFirst.new(node, :in_edges, [], :out).to_enum
28
+ # # => #<Enumerator: Edge, Edge, ...>
29
+ #
30
+ # As an end-user, you should rarely have to instantiate a traversal class
31
+ # yourself; Node#ancestors and Node#descendants provide a more convenient
32
+ # short-cut.
33
+ class Base
34
+ # Creates a new graph traversal.
35
+ #
36
+ # start - The node from which to start traversing.
37
+ # method - The method to be used to fetch the adjacent nodes (typically
38
+ # +in+ or +out+).
39
+ # args - Additional arguments to be used when calling +method+.
40
+ # fetcher - An optional method name to be called on each adjacent item
41
+ # in order to fetch *its* adjacent items. Useful if traversing
42
+ # edges instead of nodes.
43
+ #
44
+ # Returns a new traversal.
45
+ def initialize(start, method, args = nil, fetcher = nil)
46
+ @start = start
47
+ @method = method
48
+ @args = args
49
+ @fetcher = fetcher
50
+ end
51
+
52
+ # Public: A human-readable version of the traversal.
53
+ #
54
+ # Returns a string.
55
+ def inspect
56
+ "#<#{ self.class.name } start=#{ @start.inspect } " \
57
+ "method=#{ @method.inspect }" \
58
+ "#{ @fetcher ? " fetcher=#{ @fetcher.inspect }" : '' }>"
59
+ end
60
+
61
+ # Public: The next node in the traversal.
62
+ #
63
+ # Raises a StopIteration if all reachable nodes have been visited.
64
+ #
65
+ # For example
66
+ #
67
+ # traversal.next # => #<Turbine::Node key=:one>
68
+ # traversal.next # => #<Turbine::Node key=:two>
69
+ # traversal.next # => ! StopIteration
70
+ #
71
+ # Returns a Node.
72
+ def next
73
+ @fiber.resume
74
+ end
75
+
76
+ # Public: The traversal as an enumerator. This is the main way to
77
+ # traverse since the enumerator implements +each+, +map+, +with_index+,
78
+ # etc.
79
+ #
80
+ # Returns an Enumerator.
81
+ def to_enum
82
+ Enumerator.new do |control|
83
+ rewind
84
+ loop { control.yield(self.next) }
85
+ end
86
+ end
87
+
88
+ #######
89
+ private
90
+ #######
91
+
92
+ # Internal: Given a +node+ iterates through each of it's adjacent nodes
93
+ # using the +method+ and +args+ supplied when initializing the
94
+ # DepthFirst instance.
95
+ #
96
+ # When the node itself has matching adjacent nodes, those will also be
97
+ # visited. If there are loops within the graph, they will not be
98
+ # followed; each node is visited no more than once.
99
+ #
100
+ # node - The node from which to traverse.
101
+ # block - A block executed for each matching node.
102
+ #
103
+ # Returns nothing.
104
+ def visit(node, &block)
105
+ raise NotImplementedError, 'Define visit in a subclass'
106
+ end
107
+
108
+ # Internal: Fetches the next iteration item. If the traversal was
109
+ # initialized with a +fetcher+, this is called on the item, otherwise
110
+ # the item is returned untouched.
111
+ #
112
+ # Useful when traversing edges instead of nodes.
113
+ #
114
+ # Returns an object.
115
+ def fetch(adjacent)
116
+ @fetcher ? adjacent.public_send(@fetcher) : adjacent
117
+ end
118
+
119
+ # Internal: Resets the traversal to restart from the beginning.
120
+ #
121
+ # Returns nothing.
122
+ def rewind
123
+ @seen = { @start => true }
124
+
125
+ @fiber = Fiber.new do
126
+ visit(@start) { |*args| Fiber.yield(*args) }
127
+ raise StopIteration
128
+ end
129
+ end
130
+
131
+ end # Base
132
+ end # Traversal
133
+ end # Turbine