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