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