piece_pipe 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/documentation_todo.txt +0 -2
- data/lib/piece_pipe.rb +1 -0
- data/lib/piece_pipe/assembly_step.rb +29 -0
- data/lib/piece_pipe/group_by_step.rb +34 -0
- data/lib/piece_pipe/hashed_aggregator.rb +14 -0
- data/lib/piece_pipe/map_step.rb +19 -0
- data/lib/piece_pipe/method_assembly_step.rb +12 -0
- data/lib/piece_pipe/method_step.rb +21 -0
- data/lib/piece_pipe/pipeline.rb +6 -0
- data/lib/piece_pipe/version.rb +1 -1
- data/piece_pipe.gemspec +1 -0
- data/spec/group_by_step_spec.rb +30 -0
- data/spec/pipeline_spec.rb +19 -0
- data/spec/spec_helper.rb +1 -0
- metadata +49 -9
data/documentation_todo.txt
CHANGED
data/lib/piece_pipe.rb
CHANGED
@@ -6,6 +6,7 @@ require 'piece_pipe/collector'
|
|
6
6
|
require 'piece_pipe/debug_step'
|
7
7
|
require 'piece_pipe/hashed_aggregator'
|
8
8
|
require 'piece_pipe/map_step'
|
9
|
+
require 'piece_pipe/group_by_step'
|
9
10
|
require 'piece_pipe/method_assembly_step'
|
10
11
|
require 'piece_pipe/method_step'
|
11
12
|
require 'piece_pipe/tap_step'
|
@@ -1,5 +1,33 @@
|
|
1
1
|
|
2
2
|
module PiecePipe
|
3
|
+
#
|
4
|
+
# A specialization of Step, designed to operate on subsets of input and add information
|
5
|
+
# to the flow. Inputs arrive as Hashes, and outputs are generated by (effectively)
|
6
|
+
# merging new key/value pairs on the Hash and producing the result.
|
7
|
+
#
|
8
|
+
# Instead of implementing #process, AssemblySteps should implement #receive.
|
9
|
+
#
|
10
|
+
# Instead of generating output with #produce, safely generate compound output
|
11
|
+
# by calling #install with a hash of one or more interesting values. As with
|
12
|
+
# process/produce, you may call #install zero or more times, depending on your
|
13
|
+
# need to filter, transform or expand the stream of objects.
|
14
|
+
#
|
15
|
+
# The input to #receive is a duplicate of the actual Hash object that was
|
16
|
+
# produced by the previous step, so modifying the inputs directly will
|
17
|
+
# neither help you, nor harm subsequent steps.
|
18
|
+
#
|
19
|
+
# AssemblySteps insist on consuming Hash-like objects; though the preceding step
|
20
|
+
# in the pipeline needn't actually be an AssemblyStep, it must produce Hash-like
|
21
|
+
# objects as output.
|
22
|
+
#
|
23
|
+
# #install starts with a fresh copy of the inputs each time, so multiple
|
24
|
+
# invocations will not share the installed values.
|
25
|
+
#
|
26
|
+
# IF YOUR STEP DECIDES NOT TO INSTALL ANYTHING: be sure to call #noop
|
27
|
+
# (equiv to calling #install({})) which will pass all inputs on to the next
|
28
|
+
# step of the pipeline unmodified. If you DON'T call #install, your
|
29
|
+
# step effectively becomes a filter, and the flow of objects will cease.
|
30
|
+
#
|
3
31
|
class AssemblyStep < Step
|
4
32
|
def process(item)
|
5
33
|
ensure_hash_like_object item
|
@@ -11,6 +39,7 @@ module PiecePipe
|
|
11
39
|
end
|
12
40
|
end
|
13
41
|
|
42
|
+
# Default implementation is a noop
|
14
43
|
def receive(assembly)
|
15
44
|
noop
|
16
45
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PiecePipe
|
2
|
+
|
3
|
+
# does a map / aggregate for you
|
4
|
+
class GroupByStep < Step
|
5
|
+
def initialize(*keys)
|
6
|
+
@hash = {}
|
7
|
+
@keys = keys
|
8
|
+
end
|
9
|
+
|
10
|
+
def process(item)
|
11
|
+
key = nil
|
12
|
+
if @keys.size > 1
|
13
|
+
key = []
|
14
|
+
@keys.each do |k|
|
15
|
+
key << item[k]
|
16
|
+
end
|
17
|
+
else
|
18
|
+
key = item[@keys.first]
|
19
|
+
end
|
20
|
+
|
21
|
+
val = item
|
22
|
+
@hash[key] ||= []
|
23
|
+
@hash[key] << item
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_sequence
|
27
|
+
super
|
28
|
+
@hash.each do |key,values|
|
29
|
+
produce key => values
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -1,4 +1,16 @@
|
|
1
1
|
module PiecePipe
|
2
|
+
#
|
3
|
+
# This Step acts like a sink: it will process ALL available inputs until the stream ends,
|
4
|
+
# and THEN call #aggregrate multiple times to transform accumulated data and produce output.
|
5
|
+
#
|
6
|
+
# For every key-value pair that arrives as an input (see MapStep for generating key-value pairs),
|
7
|
+
# an array of values is accumulated per key.
|
8
|
+
#
|
9
|
+
# When all inputs have been collected, #aggregate is called for each key that has been seen to-date.
|
10
|
+
#
|
11
|
+
# Your job is to implement #aggregate, perform some calculation on the key and its accumulated
|
12
|
+
# values, and generate output via zero or more calls to #produce.
|
13
|
+
#
|
2
14
|
class HashedAggregator < Step
|
3
15
|
def initialize
|
4
16
|
@hash = {}
|
@@ -19,6 +31,8 @@ module PiecePipe
|
|
19
31
|
@hash[item[:key]] << item[:value]
|
20
32
|
end
|
21
33
|
|
34
|
+
# Accepts a key and an array of all values that have been collected for that key.
|
35
|
+
# Default impl simply produces { :key => key, :value => values } (essentially a noop).
|
22
36
|
def aggregate(key, values)
|
23
37
|
produce key: key, values: values
|
24
38
|
end
|
data/lib/piece_pipe/map_step.rb
CHANGED
@@ -1,4 +1,23 @@
|
|
1
1
|
module PiecePipe
|
2
|
+
#
|
3
|
+
# This type of Step is designed to produce key-value pairs in anticipation of
|
4
|
+
# a following aggregation step (see HashedAggregator).
|
5
|
+
#
|
6
|
+
# Subclasses implement #map and can produce output via #emit(key, value).
|
7
|
+
# Zero-or-more pairs may be generted per #map invocation.
|
8
|
+
#
|
9
|
+
# The output structure is in fact a Hash with two keys: :key and :value, eg
|
10
|
+
#
|
11
|
+
# class CalorieCounter < PiecePipe::MapStep
|
12
|
+
# # Expects inputs to be a Hash with :person, :date and :calories.
|
13
|
+
# # Output will look like: { :key => ["Dave","2012-05-10"], :value => 6000 }
|
14
|
+
# def map(inputs)
|
15
|
+
# key = [inputs[:person], inputs[:date]]
|
16
|
+
# val = inputs[:calories]
|
17
|
+
# emit key, val
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
2
21
|
class MapStep < Step
|
3
22
|
def process(item)
|
4
23
|
map item
|
@@ -1,4 +1,16 @@
|
|
1
1
|
module PiecePipe
|
2
|
+
# Essentially the same thing as MethodStep with AssemblyStep semantics.
|
3
|
+
#
|
4
|
+
# Because we don't have a clever way to determine that you might want to create
|
5
|
+
# an assembly line out of wrapped methods, you need to use the conveneince method Pipeline#assembly_step, eg.
|
6
|
+
#
|
7
|
+
# PiecePipe::Pipeline.new.assembly_step(:add_seatbelts)
|
8
|
+
#
|
9
|
+
# Currently only supports arity-1 methods: the parameter will be the inputs Hash,
|
10
|
+
# the return is expected to be a Hash that will be produced via AssemblyStep#install.
|
11
|
+
#
|
12
|
+
# TODO; add support for arity-2 method wrapping to allow for zero-or-more #intall calls
|
13
|
+
#
|
2
14
|
class MethodAssemblyStep < AssemblyStep
|
3
15
|
def initialize(meth)
|
4
16
|
@method = meth
|
@@ -1,4 +1,25 @@
|
|
1
1
|
module PiecePipe
|
2
|
+
# MethodStep makes it possible to define a pipeline step by wrapping a Method
|
3
|
+
# object. Eg,
|
4
|
+
# PiecePipe::Pipeline.new.step( method(:do_something) )
|
5
|
+
#
|
6
|
+
# In this contrived example, whatever class is building the Pipeline has
|
7
|
+
# a #do_something method, which is accessed as a Method object from Ruby's
|
8
|
+
# reflection API via Kernel#method.
|
9
|
+
#
|
10
|
+
# If the wrapped method accepts a single parameter (arity 1), the inputs
|
11
|
+
# to your MethodStep will be passed in via this parameter, and the return
|
12
|
+
# value of your method will be automatically #produced, even if you
|
13
|
+
# return nil.
|
14
|
+
#
|
15
|
+
# If the wrapped method accepts TWO parameters (arity 2), the inputs will
|
16
|
+
# still arrive via the first paramter, and the second parameter will
|
17
|
+
# be a "producer" object that responds to #produce. This gives you the
|
18
|
+
# flexibility to #produce zero-or-many items, as in normal Step
|
19
|
+
# subclasses. In the case of arity-2 methods, the return value
|
20
|
+
# is disregarded. Failing to call producer#produce will result
|
21
|
+
# in a filtering-out of objects.
|
22
|
+
#
|
2
23
|
class MethodStep < Step
|
3
24
|
def initialize(meth)
|
4
25
|
raise "method cannot be nil" if meth.nil?
|
data/lib/piece_pipe/pipeline.rb
CHANGED
@@ -35,6 +35,12 @@ module PiecePipe
|
|
35
35
|
add_step step
|
36
36
|
end
|
37
37
|
|
38
|
+
# like rails group by, takes the entire row and keys it based on
|
39
|
+
# the fields
|
40
|
+
def group_by(*keys)
|
41
|
+
add_step(GroupByStep.new(*keys))
|
42
|
+
end
|
43
|
+
|
38
44
|
def add_step(step)
|
39
45
|
step.source = @latest_source
|
40
46
|
@latest_source = step
|
data/lib/piece_pipe/version.rb
CHANGED
data/piece_pipe.gemspec
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PiecePipe::GroupByStep do
|
4
|
+
|
5
|
+
context "grouping by a subset" do
|
6
|
+
|
7
|
+
it "groups by a single key" do
|
8
|
+
subject = PiecePipe::GroupByStep.new(:last)
|
9
|
+
inputs = [ { first: 'David', last: 'Crosby', age: 65 },
|
10
|
+
{ first: 'Bill', last: 'Crosby', age: 67 } ]
|
11
|
+
|
12
|
+
PiecePipe::Pipeline.new.
|
13
|
+
source(inputs).
|
14
|
+
step(subject).to_a.first.should == {
|
15
|
+
"Crosby" => inputs
|
16
|
+
}
|
17
|
+
|
18
|
+
# ezpipe(subject,inputs)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "groups by multiple keys" do
|
22
|
+
subject = PiecePipe::GroupByStep.new(:last, :first)
|
23
|
+
inputs = { first: 'David', last: 'Crosby', age: 65 }
|
24
|
+
ezpipe(subject,inputs).to_a.first.should == {
|
25
|
+
["Crosby", 'David'] => [inputs]
|
26
|
+
}
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/pipeline_spec.rb
CHANGED
@@ -87,4 +87,23 @@ describe PiecePipe::Pipeline do
|
|
87
87
|
end
|
88
88
|
end
|
89
89
|
|
90
|
+
describe "#group_by" do
|
91
|
+
let(:project_info) {
|
92
|
+
[
|
93
|
+
{name: "Project 1", project_health_summary: "Summary 1"},
|
94
|
+
{name: "Project 2", project_health_summary: "Summary 2"},
|
95
|
+
]
|
96
|
+
}
|
97
|
+
|
98
|
+
it "maps the pipeline items down to the values indicated by the given key" do
|
99
|
+
subject.
|
100
|
+
source(project_info).
|
101
|
+
group_by(:name)
|
102
|
+
|
103
|
+
p1 = {"Project 1" => [ {name: "Project 1", project_health_summary: "Summary 1"} ]}
|
104
|
+
p2 = {"Project 2" => [ {name: "Project 2", project_health_summary: "Summary 2"} ]}
|
105
|
+
subject.to_enum.to_a.should == [ p1, p2]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
90
109
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: piece_pipe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,11 +10,11 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-09-04 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: mocha
|
17
|
-
requirement:
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ! '>='
|
@@ -22,10 +22,15 @@ dependencies:
|
|
22
22
|
version: '0'
|
23
23
|
type: :development
|
24
24
|
prerelease: false
|
25
|
-
version_requirements:
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
26
31
|
- !ruby/object:Gem::Dependency
|
27
32
|
name: rspec
|
28
|
-
requirement:
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
29
34
|
none: false
|
30
35
|
requirements:
|
31
36
|
- - ! '>='
|
@@ -33,10 +38,31 @@ dependencies:
|
|
33
38
|
version: '0'
|
34
39
|
type: :development
|
35
40
|
prerelease: false
|
36
|
-
version_requirements:
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
37
47
|
- !ruby/object:Gem::Dependency
|
38
48
|
name: rake
|
39
|
-
requirement:
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: pry
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
40
66
|
none: false
|
41
67
|
requirements:
|
42
68
|
- - ! '>='
|
@@ -44,7 +70,12 @@ dependencies:
|
|
44
70
|
version: '0'
|
45
71
|
type: :development
|
46
72
|
prerelease: false
|
47
|
-
version_requirements:
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
48
79
|
description: ! 'PiecePipe is about breaking your problem into its smallest, most interesting
|
49
80
|
pieces, solving those pieces and not spending time on the glue code between them. '
|
50
81
|
email:
|
@@ -65,6 +96,7 @@ files:
|
|
65
96
|
- lib/piece_pipe/assembly_step.rb
|
66
97
|
- lib/piece_pipe/collector.rb
|
67
98
|
- lib/piece_pipe/debug_step.rb
|
99
|
+
- lib/piece_pipe/group_by_step.rb
|
68
100
|
- lib/piece_pipe/hashed_aggregator.rb
|
69
101
|
- lib/piece_pipe/map_step.rb
|
70
102
|
- lib/piece_pipe/method_assembly_step.rb
|
@@ -76,6 +108,7 @@ files:
|
|
76
108
|
- piece_pipe.gemspec
|
77
109
|
- spec/assembly_step_spec.rb
|
78
110
|
- spec/collector_spec.rb
|
111
|
+
- spec/group_by_step_spec.rb
|
79
112
|
- spec/hashed_aggregator_spec.rb
|
80
113
|
- spec/map_step_spec.rb
|
81
114
|
- spec/method_assembly_step_spec.rb
|
@@ -95,15 +128,21 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
128
|
- - ! '>='
|
96
129
|
- !ruby/object:Gem::Version
|
97
130
|
version: '0'
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
hash: 2537402222991668823
|
98
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
135
|
none: false
|
100
136
|
requirements:
|
101
137
|
- - ! '>='
|
102
138
|
- !ruby/object:Gem::Version
|
103
139
|
version: '0'
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
hash: 2537402222991668823
|
104
143
|
requirements: []
|
105
144
|
rubyforge_project:
|
106
|
-
rubygems_version: 1.8.
|
145
|
+
rubygems_version: 1.8.24
|
107
146
|
signing_key:
|
108
147
|
specification_version: 3
|
109
148
|
summary: PiecePipe helps you break your code into small interesting pieces and provides
|
@@ -111,6 +150,7 @@ summary: PiecePipe helps you break your code into small interesting pieces and p
|
|
111
150
|
test_files:
|
112
151
|
- spec/assembly_step_spec.rb
|
113
152
|
- spec/collector_spec.rb
|
153
|
+
- spec/group_by_step_spec.rb
|
114
154
|
- spec/hashed_aggregator_spec.rb
|
115
155
|
- spec/map_step_spec.rb
|
116
156
|
- spec/method_assembly_step_spec.rb
|