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.
@@ -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