piece_pipe 0.1.0 → 0.2.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/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
|