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.
- data/Gemfile +15 -0
- data/Guardfile +6 -0
- data/LICENSE +27 -0
- data/README.md +189 -0
- data/Rakefile +126 -0
- data/examples/energy.rb +80 -0
- data/examples/family.rb +125 -0
- data/lib/turbine.rb +29 -0
- data/lib/turbine/algorithms/filtered_tarjan.rb +33 -0
- data/lib/turbine/algorithms/tarjan.rb +50 -0
- data/lib/turbine/edge.rb +94 -0
- data/lib/turbine/errors.rb +74 -0
- data/lib/turbine/graph.rb +113 -0
- data/lib/turbine/node.rb +246 -0
- data/lib/turbine/pipeline/README.mdown +31 -0
- data/lib/turbine/pipeline/dsl.rb +275 -0
- data/lib/turbine/pipeline/expander.rb +67 -0
- data/lib/turbine/pipeline/filter.rb +52 -0
- data/lib/turbine/pipeline/journal.rb +130 -0
- data/lib/turbine/pipeline/journal_filter.rb +71 -0
- data/lib/turbine/pipeline/pump.rb +19 -0
- data/lib/turbine/pipeline/segment.rb +175 -0
- data/lib/turbine/pipeline/sender.rb +51 -0
- data/lib/turbine/pipeline/split.rb +132 -0
- data/lib/turbine/pipeline/trace.rb +55 -0
- data/lib/turbine/pipeline/transform.rb +49 -0
- data/lib/turbine/pipeline/traversal.rb +34 -0
- data/lib/turbine/pipeline/unique.rb +47 -0
- data/lib/turbine/properties.rb +48 -0
- data/lib/turbine/traversal/base.rb +133 -0
- data/lib/turbine/traversal/breadth_first.rb +49 -0
- data/lib/turbine/traversal/depth_first.rb +46 -0
- data/lib/turbine/version.rb +4 -0
- data/turbine.gemspec +84 -0
- metadata +120 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# A Pipeline segment which expands arrays, sets, and enumerators such
|
4
|
+
# that, instead of sending them to the next segment, each member of the
|
5
|
+
# collection is sent separately.
|
6
|
+
#
|
7
|
+
# pump = Pump.new(%w( abc def ), %w( hik klm ))
|
8
|
+
# expand = Expander.new
|
9
|
+
#
|
10
|
+
# pipeline = pump | expand
|
11
|
+
#
|
12
|
+
# pipeline.next # => 'abc'
|
13
|
+
# pipeline.next # => 'def'
|
14
|
+
# pipeline.next # => 'hij'
|
15
|
+
# pipeline.next # => 'klm'
|
16
|
+
#
|
17
|
+
# Using Expander on a pipeline has the same effect as calling +flatten(1)+
|
18
|
+
# on an Array.
|
19
|
+
class Expander < Segment
|
20
|
+
|
21
|
+
# Public: Creates a new Expander segment.
|
22
|
+
#
|
23
|
+
# Returns an Expander.
|
24
|
+
def initialize
|
25
|
+
super
|
26
|
+
@buffer = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Runs the pipeline once returning the next value.
|
30
|
+
#
|
31
|
+
# If a recent call to +next+ returned an array, set or enumerator, the
|
32
|
+
# next value from that collection will be emitted instead. Only once all
|
33
|
+
# of the values in the buffer have been emitted will the upstream
|
34
|
+
# segment be called again.
|
35
|
+
#
|
36
|
+
# Returns an object.
|
37
|
+
def next
|
38
|
+
if from_buffer = input_from_buffer
|
39
|
+
handle_value(from_buffer)
|
40
|
+
else
|
41
|
+
case value = super
|
42
|
+
when Array, Set, Enumerator
|
43
|
+
# Recurse into arrays, as the input may return multiple results (as
|
44
|
+
# is commonly the case when calling Node#in, Node#descendants, etc).
|
45
|
+
@buffer = value.to_a.compact
|
46
|
+
@buffer.any? ? handle_value(@buffer.shift) : self.next
|
47
|
+
else
|
48
|
+
handle_value(value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#######
|
54
|
+
private
|
55
|
+
#######
|
56
|
+
|
57
|
+
# Internal: If there are any buffered values yet to be emitted, returns
|
58
|
+
# the next one; otherwise returns nil.
|
59
|
+
#
|
60
|
+
# Returns an object or nil.
|
61
|
+
def input_from_buffer
|
62
|
+
@buffer && @buffer.any? && @buffer.shift
|
63
|
+
end
|
64
|
+
|
65
|
+
end # Expander
|
66
|
+
end # Pipeline
|
67
|
+
end # Turbine
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# Emits only those values in the pipeline which satisfy the +filter+.
|
4
|
+
# Filter is to Pipeline as +select+ is to Enumerable.
|
5
|
+
#
|
6
|
+
# Filter.new { |x| firewall.permitted?(x) }
|
7
|
+
#
|
8
|
+
class Filter < Segment
|
9
|
+
include Trace::Transparent
|
10
|
+
|
11
|
+
# Public: Creates a new Filter segment.
|
12
|
+
#
|
13
|
+
# You may opt to use the Filter class directly, passing a block when
|
14
|
+
# initializing which is used to control which values are emitted.
|
15
|
+
# Alternatively, provide no block and use a subclass with a custom
|
16
|
+
# +filter+ method.
|
17
|
+
#
|
18
|
+
# Without a filter block, all elements are emitted.
|
19
|
+
#
|
20
|
+
# block - An optional block used to select which values are emitted by
|
21
|
+
# the filter.
|
22
|
+
#
|
23
|
+
# Returns a Filter.
|
24
|
+
def initialize(&block)
|
25
|
+
@filter = (block || method(:filter))
|
26
|
+
super()
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Runs the pipeline continuously until a value which matches the
|
30
|
+
# filter is found, then that value is emitted.
|
31
|
+
#
|
32
|
+
# Returns an object.
|
33
|
+
def next
|
34
|
+
# Discard non-matching values.
|
35
|
+
nil until (value = input) && @filter.call(value)
|
36
|
+
|
37
|
+
handle_value(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
#######
|
41
|
+
private
|
42
|
+
#######
|
43
|
+
|
44
|
+
# Internal: The default filter condition.
|
45
|
+
#
|
46
|
+
# Returns true or false.
|
47
|
+
def filter(value)
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end # Filter
|
51
|
+
end # Pipeline
|
52
|
+
end # Turbine
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# Journal segments keep track of all of the values emitted by the source
|
4
|
+
# segment so that they can be used later in the pipeline.
|
5
|
+
class Journal < Segment
|
6
|
+
include Trace::Transparent
|
7
|
+
|
8
|
+
# The name used to refer to the segments values later in the pipeline.
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# Public: Creates a new Journal segment.
|
12
|
+
#
|
13
|
+
# name - A name which is associated with the values remembered by the
|
14
|
+
# segment. This is required so that the values can be referred to
|
15
|
+
# in later segments.
|
16
|
+
#
|
17
|
+
# Returns a Journal instance.
|
18
|
+
def initialize(name)
|
19
|
+
super()
|
20
|
+
|
21
|
+
@name = name
|
22
|
+
forget!
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: The values stored by the segment; contains the results of
|
26
|
+
# running the upstream segments on all the inputs.
|
27
|
+
#
|
28
|
+
# Returns an array.
|
29
|
+
def values
|
30
|
+
@values ||= @source.to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
# Public: Run the pipeline once, returning the next value. Forces
|
34
|
+
# complete evalulation of the values emitted by the upstream segments.
|
35
|
+
#
|
36
|
+
# See Segment#next.
|
37
|
+
#
|
38
|
+
# Returns the next value.
|
39
|
+
def next
|
40
|
+
values
|
41
|
+
|
42
|
+
if @index >= values.length - 1
|
43
|
+
# We test length-1 because the index is always one position *lower*
|
44
|
+
# than the actual position at this stage; it is incremented later
|
45
|
+
# when calling +input+.
|
46
|
+
raise StopIteration
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public: Rewinds the segment so that iteration can happen from the
|
53
|
+
# first in put again.
|
54
|
+
#
|
55
|
+
# Returns nothing.
|
56
|
+
def rewind
|
57
|
+
forget!
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
# Public: Checks if the given +value+ is stored in the Journal. Faster
|
62
|
+
# than +values.include?+.
|
63
|
+
#
|
64
|
+
# value - The value to look for.
|
65
|
+
#
|
66
|
+
# Returns true or false.
|
67
|
+
def include?(value)
|
68
|
+
@lookup ||= Set.new(values)
|
69
|
+
@lookup.include?(value)
|
70
|
+
end
|
71
|
+
|
72
|
+
alias_method :member?, :include?
|
73
|
+
|
74
|
+
# Public: Describes the segments through which each input will pass.
|
75
|
+
#
|
76
|
+
# Return a string.
|
77
|
+
def to_s
|
78
|
+
"#{ source_to_s } | as(#{ @name.inspect })"
|
79
|
+
end
|
80
|
+
|
81
|
+
#######
|
82
|
+
private
|
83
|
+
#######
|
84
|
+
|
85
|
+
# Internal: Resets the Journal so that previously computed values are
|
86
|
+
# forgotten.
|
87
|
+
#
|
88
|
+
# Returns nothing.
|
89
|
+
def forget!
|
90
|
+
@values = nil
|
91
|
+
@lookup = nil
|
92
|
+
@index = -1
|
93
|
+
end
|
94
|
+
|
95
|
+
# Internal: Retrieves the next value emitted by the upstream segment.
|
96
|
+
#
|
97
|
+
# Returns an object.
|
98
|
+
def input
|
99
|
+
@values[@index += 1]
|
100
|
+
end
|
101
|
+
|
102
|
+
# A collection of methods providing segment classes with the means to
|
103
|
+
# easily access values stored in an upstream Journal.
|
104
|
+
module Read
|
105
|
+
# Public: Retrieves the values stored in the upstream journal whose
|
106
|
+
# name is +name+.
|
107
|
+
#
|
108
|
+
# name - The name of the upstream journal.
|
109
|
+
#
|
110
|
+
# Raises a NoSuchJournalError if the given +name+ does not match a
|
111
|
+
# journal in the pipeline.
|
112
|
+
#
|
113
|
+
# Returns an array of values.
|
114
|
+
def journal(name)
|
115
|
+
upstream = source
|
116
|
+
|
117
|
+
while upstream.kind_of?(Segment)
|
118
|
+
if upstream.is_a?(Journal) && upstream.name == name
|
119
|
+
return upstream
|
120
|
+
end
|
121
|
+
|
122
|
+
upstream = upstream.source
|
123
|
+
end
|
124
|
+
|
125
|
+
raise NoSuchJournalError.new(name)
|
126
|
+
end
|
127
|
+
end # Read
|
128
|
+
end # Journal
|
129
|
+
end # Pipeline
|
130
|
+
end # Turbine
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# A filter which uses an upstream Journal as a basis for determining which
|
4
|
+
# values should be emitted.
|
5
|
+
class JournalFilter < Filter
|
6
|
+
include Journal::Read
|
7
|
+
|
8
|
+
# Public: Creates a new JournalFilter.
|
9
|
+
#
|
10
|
+
# mode - The filter mode to be used. :only will cause the filter to emit
|
11
|
+
# only values which were seen in the journal, while :except will
|
12
|
+
# emit only those which were *not* seen.
|
13
|
+
# name - The name of the Journal whose values will be used.
|
14
|
+
#
|
15
|
+
# Returns a JournalFilter.
|
16
|
+
def initialize(mode, name)
|
17
|
+
unless mode == :only || mode == :except
|
18
|
+
raise ArgumentError, 'JournalFilter mode must be :only or :except'
|
19
|
+
end
|
20
|
+
|
21
|
+
@mode = mode
|
22
|
+
@journal_name = name
|
23
|
+
|
24
|
+
super(&method(:"filter_#{ mode }"))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Public: Sets the previous segment in the pipeline.
|
28
|
+
#
|
29
|
+
# Raises NoSuchJournalError if the Journal required by the filter is not
|
30
|
+
# present in the source pipeline.
|
31
|
+
#
|
32
|
+
# Returns the source.
|
33
|
+
def source=(source)
|
34
|
+
super
|
35
|
+
@journal = journal(@journal_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Describes the segments through which each input will pass.
|
39
|
+
#
|
40
|
+
# Return a string.
|
41
|
+
def to_s
|
42
|
+
"#{ source_to_s } | #{ @mode }(#{ @journal_name.inspect })"
|
43
|
+
end
|
44
|
+
|
45
|
+
#######
|
46
|
+
private
|
47
|
+
#######
|
48
|
+
|
49
|
+
# Internal: Filter mode which emits only the values which were seen in
|
50
|
+
# the journal.
|
51
|
+
#
|
52
|
+
# value - The value emitted by the source.
|
53
|
+
#
|
54
|
+
# Returns true or false.
|
55
|
+
def filter_only(value)
|
56
|
+
@journal.include?(value)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Internal: Filter mode which emits only the values which were not seen
|
60
|
+
# in the journal.
|
61
|
+
#
|
62
|
+
# value - The value emitted by the source.
|
63
|
+
#
|
64
|
+
# Returns true or false.
|
65
|
+
def filter_except(value)
|
66
|
+
not filter_only(value)
|
67
|
+
end
|
68
|
+
|
69
|
+
end # Only
|
70
|
+
end # Pipeline
|
71
|
+
end # Turbine
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# A pipeline segment which acts as the source for the pipeline. Values
|
4
|
+
# from the pump are "piped" through each segment in the pipeline.
|
5
|
+
class Pump < Segment
|
6
|
+
# Public: Creates a new pump upon whose elements the pipeline will act.
|
7
|
+
#
|
8
|
+
# source - The elements in the source which will pass through the
|
9
|
+
# pipeline. Anything which responds to #each is acceptable,
|
10
|
+
# including arrays, Turbine collections, and enumerators.
|
11
|
+
#
|
12
|
+
# Returns a Pump.
|
13
|
+
def initialize(source)
|
14
|
+
@source = source.to_enum
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
end # Pump
|
18
|
+
end # Pipeline
|
19
|
+
end # Turbine
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Turbine
|
2
|
+
module Pipeline
|
3
|
+
# Represents a single stage in a pipeline. A pipeline may contain many
|
4
|
+
# segments, each of which transform or filter the elements which pass
|
5
|
+
# through it.
|
6
|
+
class Segment
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# The previous segment in the pipeline.
|
10
|
+
attr_accessor :source
|
11
|
+
|
12
|
+
# Public: Creates a new Segment. Segment itself is of little value in
|
13
|
+
# your pipelines as it will simply emit every value it is given. Instead
|
14
|
+
# you should look to Pump, Transform, and Filter.
|
15
|
+
#
|
16
|
+
# Returns a Segment.
|
17
|
+
def initialize
|
18
|
+
@tracing = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Appends +other+ segment to be given the values emitted by this
|
22
|
+
# segment. Instead of a Segment instance, a block can be given instead
|
23
|
+
# which is used as a Transform.
|
24
|
+
#
|
25
|
+
# other - The segment or transform block to be run after this segment.
|
26
|
+
#
|
27
|
+
# For example, transforming three numbers using a Transform segment:
|
28
|
+
#
|
29
|
+
# Pump.new([10, 20, 30]).append(Transform.new { |x| Math.sqrt(x) })
|
30
|
+
#
|
31
|
+
# Or using a lambda:
|
32
|
+
#
|
33
|
+
# Pump.new([10, 20, 30]).append(->(x) { x ** 10 })
|
34
|
+
#
|
35
|
+
# Or using Dave Thomas' pipes syntax:
|
36
|
+
#
|
37
|
+
# pump = Pump.new((100..10000).to_a)
|
38
|
+
# divide = Transform.new { |x| x / 100 }
|
39
|
+
# the_answer = Filter.new { |x| x == 42 }
|
40
|
+
#
|
41
|
+
# (pump | divide | the_answer).next # => 42
|
42
|
+
#
|
43
|
+
# Returns the +other+ segment.
|
44
|
+
def append(other)
|
45
|
+
if other.respond_to?(:call)
|
46
|
+
other = Transform.new(&other)
|
47
|
+
end
|
48
|
+
|
49
|
+
other.source = self
|
50
|
+
other
|
51
|
+
end
|
52
|
+
|
53
|
+
alias_method :|, :append
|
54
|
+
|
55
|
+
# Public: Runs the pipeline once, returning the next value. Repeatedly
|
56
|
+
# calling this will yield each value in turn. Once all values have been
|
57
|
+
# emitted a StopIteration is raised (mimicking the behaviour of
|
58
|
+
# Enumerator).
|
59
|
+
#
|
60
|
+
# Returns an object.
|
61
|
+
def next
|
62
|
+
handle_value(input)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Public: Iterates through each value in the pipeline.
|
66
|
+
#
|
67
|
+
# Returns nothing.
|
68
|
+
def each
|
69
|
+
rewind
|
70
|
+
loop { yield self.next }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Public: Rewinds the segment so that iteration can happen from the
|
74
|
+
# first input again.
|
75
|
+
#
|
76
|
+
# Returns nothing.
|
77
|
+
def rewind
|
78
|
+
@source.rewind
|
79
|
+
@previous = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Enables tracing on the segment and it's source. This tells the
|
83
|
+
# segment to keep track of the most recently emitted value for use in a
|
84
|
+
# subsequent Trace segment.
|
85
|
+
#
|
86
|
+
# Returns the tracing setting.
|
87
|
+
def tracing=(use_tracing)
|
88
|
+
@tracing = use_tracing
|
89
|
+
|
90
|
+
if @source && @source.respond_to?(:tracing=)
|
91
|
+
@source.tracing = use_tracing
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Public: Returns the trace containing the most recently emitted values
|
96
|
+
# for all the source segments, appending this segment's value to the end
|
97
|
+
# of the array.
|
98
|
+
#
|
99
|
+
# For example
|
100
|
+
#
|
101
|
+
# segment.next && segment.trace
|
102
|
+
# # => [[ #<Node key=:jay>, #<Node key=:claire>, #<Node key=:haley> ]]
|
103
|
+
#
|
104
|
+
# segment.next && segment.trace
|
105
|
+
# # => [[ #<Node key=:jay>, #<Node key=:claire>, #<Node key=:alex> ]]
|
106
|
+
#
|
107
|
+
# Tracing must be enabled (normally by appending a Trace segment to the
|
108
|
+
# pipeline) otherwise a TracingNotEnabledError is raised.
|
109
|
+
#
|
110
|
+
# Subclasses may call +super+ with a block; the sole argument given to
|
111
|
+
# the block will be trace from the source segments.
|
112
|
+
#
|
113
|
+
# Returns an array.
|
114
|
+
def trace
|
115
|
+
unless @tracing
|
116
|
+
raise TracingNotEnabledError.new(self)
|
117
|
+
end
|
118
|
+
|
119
|
+
trace = @source.respond_to?(:trace) ? @source.trace.dup : []
|
120
|
+
block_given? ? yield(trace) : trace.push(@previous)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Public: Describes the segments through which each input will pass.
|
124
|
+
#
|
125
|
+
# For example:
|
126
|
+
#
|
127
|
+
# pipeline.to_s
|
128
|
+
# # => "Pump | Sender(out) | Filter"
|
129
|
+
#
|
130
|
+
# Returns a string.
|
131
|
+
def to_s
|
132
|
+
name = self.class.name
|
133
|
+
|
134
|
+
# Nicked from ActiveSupport since it's faster than gsub, and more
|
135
|
+
# memory-efficient than split.
|
136
|
+
name = (index = name.rindex('::')) ? name[(index + 2)..-1] : name
|
137
|
+
|
138
|
+
source_string = source_to_s
|
139
|
+
source_string.nil? ? name : "#{ source_string } | #{ name }"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Public: A human-readable version of the segment for debugging.
|
143
|
+
#
|
144
|
+
# Returns a String.
|
145
|
+
def inspect
|
146
|
+
to_s
|
147
|
+
end
|
148
|
+
|
149
|
+
#######
|
150
|
+
private
|
151
|
+
#######
|
152
|
+
|
153
|
+
def input
|
154
|
+
nil until (value = source.next)
|
155
|
+
value
|
156
|
+
end
|
157
|
+
|
158
|
+
def output(value)
|
159
|
+
if @tracing
|
160
|
+
@previous = value
|
161
|
+
end
|
162
|
+
|
163
|
+
value
|
164
|
+
end
|
165
|
+
|
166
|
+
def handle_value(value)
|
167
|
+
output(value)
|
168
|
+
end
|
169
|
+
|
170
|
+
def source_to_s
|
171
|
+
@source.is_a?(Segment) ? @source.to_s : nil
|
172
|
+
end
|
173
|
+
end # Segment
|
174
|
+
end # Pipeline
|
175
|
+
end # Turbine
|