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