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.
@@ -1,5 +1,3 @@
1
- MethodStep
2
- MethodAssemblyStep
3
1
  TapStep
4
2
  MapStep
5
3
  HashedAggregator
@@ -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
@@ -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?
@@ -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
@@ -1,3 +1,3 @@
1
1
  module PiecePipe
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -17,5 +17,6 @@ Gem::Specification.new do |gem|
17
17
  gem.add_development_dependency "mocha"
18
18
  gem.add_development_dependency "rspec"
19
19
  gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "pry"
20
21
 
21
22
  end
@@ -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
@@ -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
@@ -1,4 +1,5 @@
1
1
  require 'piece_pipe'
2
+ require 'pry'
2
3
 
3
4
  module SpecHelpers
4
5
  def ezpipe(single_step, inputz)
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.1.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-06-07 00:00:00.000000000Z
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: &2152338560 !ruby/object:Gem::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: *2152338560
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: &2152337340 !ruby/object:Gem::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: *2152337340
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: &2152321200 !ruby/object:Gem::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: *2152321200
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.10
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