turbine-graph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|