piece_pipe 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ pkg
2
+ *.swp
3
+ *.swo
4
+ *.log
5
+ doc
6
+ coverage
7
+ complexity
8
+ .DS_Store
9
+ .yardoc
10
+ tmp
11
+ tags
12
+ ctags
13
+ .bundle
14
+ *.lock
15
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in piece_pipe.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Atomic Object
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # PiecePipe
2
+
3
+ PiecePipe is about breaking your problem into its smallest, most interesting pieces, solving those pieces and not spending time on the glue code between them.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'piece_pipe'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install piece_pipe
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ class NuclearPowerPlantHealthSummaryGenerator
23
+ def generate(region)
24
+ PiecePipe::Pipeline.new.
25
+ source([{region: region}]).
26
+ step(FetchPowerPlantsByRegion).
27
+ step(FindWorstReactor).
28
+ step(DetermineStatusClass).
29
+ step(BuildPlantHealthSummary).
30
+ step(SortByRadiationLevelsDescending).
31
+ collect(:plant_health_summary).
32
+ to_enum
33
+ end
34
+ end
35
+ ```
36
+
37
+
38
+ ## Contributing
39
+
40
+ 1. Fork it
41
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
42
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
43
+ 4. Push to the branch (`git push origin my-new-feature`)
44
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ desc "Run all rspecs"
6
+ RSpec::Core::RakeTask.new(:spec) do |t|
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ t.rspec_opts = '-c'
9
+ end
10
+
11
+ task :default => :spec
12
+
@@ -0,0 +1,40 @@
1
+
2
+ module PiecePipe
3
+ class AssemblyStation < PipelineElement
4
+ def process(item)
5
+ ensure_hash_like_object item
6
+ @assembly = item
7
+ begin
8
+ receive(item)
9
+ ensure
10
+ @assembly = nil
11
+ end
12
+ end
13
+
14
+ def receive(assembly)
15
+ noop
16
+ end
17
+
18
+ def noop
19
+ install({})
20
+ end
21
+
22
+ def install(value,opts={})
23
+ output = @assembly.merge(value || {})
24
+
25
+ if opts[:drop]
26
+ opts[:drop].each do |key|
27
+ output.delete(key)
28
+ end
29
+ end
30
+ produce output
31
+ end
32
+
33
+ private
34
+ def ensure_hash_like_object(obj)
35
+ unless obj.respond_to?(:[]) and obj.respond_to?(:merge) and obj.respond_to?(:dup)
36
+ raise "AssemblyStation object #{self.class.name} requires its source to produce Hash-like elements; not acceptable: #{obj.inspect}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,11 @@
1
+ module PiecePipe
2
+ class Collector < PipelineElement
3
+ def initialize(key)
4
+ @key = key
5
+ end
6
+
7
+ def process(hash)
8
+ produce hash[@key]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module PiecePipe
2
+ class DebugStep < PipelineElement
3
+ def initialize(opts={},&block)
4
+ @opts = opts
5
+ @block = block
6
+ end
7
+
8
+ def process(item)
9
+ title = @opts[:title] || "DEBUG:"
10
+ if @opts[:inspect_keys]
11
+ puts "#{title} #{item.slice(*@opts[:inspect_keys])}"
12
+ end
13
+ @block.call if @block
14
+ produce item
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module PiecePipe
2
+ class HashedAggregator < PipelineElement
3
+ def initialize
4
+ @hash = {}
5
+ end
6
+
7
+ def generate_sequence
8
+ super
9
+ @hash.each do |key,values|
10
+ aggregate key, values
11
+ end
12
+ end
13
+
14
+ def process(item)
15
+ raise "HashedAggregator requires inputs to be Hashes with :key and :value" unless item.keys.include?(:key) and item.keys.include?(:value)
16
+ # TODO : check key/val keys in item
17
+ #
18
+ @hash[item[:key]] ||= []
19
+ @hash[item[:key]] << item[:value]
20
+ end
21
+
22
+ def aggregate(key, values)
23
+ produce key: key, values: values
24
+ end
25
+ end
26
+ end
27
+
@@ -0,0 +1,15 @@
1
+ module PiecePipe
2
+ class MapStep < PipelineElement
3
+ def process(item)
4
+ map item
5
+ end
6
+
7
+ def map(item)
8
+ emit item, item
9
+ end
10
+
11
+ def emit(key, value)
12
+ produce key: key, value: value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module PiecePipe
2
+ class MethodAssemblyStation < AssemblyStation
3
+ def initialize(meth)
4
+ @method = meth
5
+ end
6
+
7
+ def receive(inputs)
8
+ install(@method.call(inputs))
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ module PiecePipe
2
+ class MethodElement < PipelineElement
3
+ def initialize(meth)
4
+ raise "method cannot be nil" if meth.nil?
5
+ raise "method must accept 1 or 2 arguments; it accepts #{meth.arity}" if meth.arity != 1 and meth.arity != 2
6
+ @method = meth
7
+ end
8
+
9
+ public :produce # We will provide a ref to self as a producer for use by @method.
10
+
11
+ def process(item)
12
+ case @method.arity
13
+ when 1
14
+ # The method takes 1 argument, which we assume to be the input item.
15
+ # We also assume that the method RETURNS the ONLY item it wishes to produce.
16
+ # Will always produce exactly 1 output, even if that's just nil.
17
+ produce @method.call(item)
18
+ when 2
19
+ # The method takes 2 arguments. Assume first is input item, second is "producer",
20
+ # ie, a means for the method to produce 0, 1 or many outputs at its discretion.
21
+ # (by calling producer.produce)
22
+ # Return value of @method.call is NOT considered.
23
+ @method.call item, self
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,75 @@
1
+ module PiecePipe
2
+ class Pipeline
3
+ def initialize
4
+ @latest_source = nil
5
+ end
6
+
7
+ def source(source)
8
+ raise "Source already set to #{@latest_source.inspect}" unless @latest_source.nil?
9
+ @latest_source = source
10
+ self
11
+ end
12
+
13
+ def input(item)
14
+ source [item]
15
+ self
16
+ end
17
+
18
+ def step(step)
19
+ case step
20
+ when Class
21
+ step = step.new # If the arg is a Class, instantiate it
22
+ when Method
23
+ step = MethodElement.new(step)
24
+ end
25
+ add_step step
26
+ end
27
+
28
+ def assembly_step(step)
29
+ case step
30
+ when Class
31
+ step = step.new # If the arg is a Class, instantiate it
32
+ when Method
33
+ step = MethodAssemblyStation.new(step)
34
+ end
35
+ add_step step
36
+ end
37
+
38
+ def add_step(step)
39
+ step.source = @latest_source
40
+ @latest_source = step
41
+ self
42
+ end
43
+ private :add_step
44
+
45
+ def tap(&block)
46
+ step(TapStep.new(&block))
47
+ end
48
+
49
+ def debug(opts)
50
+ step(DebugStep.new(opts))
51
+ end
52
+
53
+ def collect(key)
54
+ step(Collector.new(key))
55
+ end
56
+
57
+ def result
58
+ to_enum.first
59
+ end
60
+
61
+ def to_enum
62
+ @latest_source.to_enum
63
+ end
64
+ alias smoke to_enum
65
+
66
+ def to_a
67
+ to_enum.to_a
68
+ end
69
+ end
70
+
71
+ def create_pipeline
72
+ ::Pipeline::Pipeline.new
73
+ end
74
+
75
+ end
@@ -0,0 +1,35 @@
1
+ module PiecePipe
2
+ class PipelineElement
3
+ attr_accessor :source
4
+
5
+ def to_enum
6
+ Enumerator.new do |yielder|
7
+ @yielder = yielder
8
+ begin
9
+ generate_sequence
10
+ ensure
11
+ @yielder = nil
12
+ end
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ def generate_sequence
19
+ if source.nil?
20
+ raise "The source of PipelineElement #{self.class.name} is nil"
21
+ end
22
+ source.to_enum.each do |item|
23
+ process(item)
24
+ end
25
+ end
26
+
27
+ def process(item)
28
+ produce item
29
+ end
30
+
31
+ def produce(something)
32
+ @yielder << something
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ module PiecePipe
2
+ class TapStep < PipelineElement
3
+ def initialize(&block)
4
+ @block = block
5
+ end
6
+
7
+ def process(item)
8
+ @block.call item
9
+ produce item
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module PiecePipe
2
+ VERSION = "0.0.1"
3
+ end
data/lib/piece_pipe.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'piece_pipe/pipeline'
2
+ require 'piece_pipe/pipeline_element'
3
+
4
+ require 'piece_pipe/assembly_station'
5
+ require 'piece_pipe/collector'
6
+ require 'piece_pipe/debug_step'
7
+ require 'piece_pipe/hashed_aggregator'
8
+ require 'piece_pipe/map_step'
9
+ require 'piece_pipe/method_assembly_station'
10
+ require 'piece_pipe/method_element'
11
+ require 'piece_pipe/tap_step'
12
+ require 'piece_pipe/version'
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/piece_pipe/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["David Crosby", "Shawn Anderson"]
6
+ gem.email = ["crosby@atomicobject.com", "shawn42@gmail.com"]
7
+ gem.description = %q{PiecePipe is about breaking your problem into its smallest, most interesting pieces, solving those pieces and not spending time on the glue code between them. }
8
+ gem.summary = %q{ PiecePipe helps you break your code into small interesting pieces and provides the glue for pipelining them together to provide elegant, readable code. }
9
+ gem.homepage = "http://atomicobject.com"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "piece_pipe"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = PiecePipe::VERSION
17
+ gem.add_development_dependency "mocha"
18
+ gem.add_development_dependency "rspec"
19
+ gem.add_development_dependency "rake"
20
+
21
+ end
@@ -0,0 +1,162 @@
1
+ require "spec_helper"
2
+
3
+ describe PiecePipe::AssemblyStation do
4
+
5
+ context "default AssemblyStation" do
6
+ it "produces unaltered items from its source" do
7
+ as = PiecePipe::AssemblyStation.new
8
+ as.source = [
9
+ {hello: "hippo"},
10
+ {hello: "lion"}
11
+ ]
12
+ as.to_enum.to_a.should == [
13
+ {hello: "hippo"},
14
+ {hello: "lion"}
15
+ ]
16
+
17
+ end
18
+ end
19
+
20
+ context "a typical AssemblyStation" do
21
+ class AnimalPhraser < PiecePipe::AssemblyStation
22
+ def receive(inputs)
23
+ install phrase: "There are #{inputs[:count]} #{inputs[:animal]}s"
24
+ end
25
+ end
26
+
27
+ it "can install new information into the work stream based on inputs" do
28
+ ap = AnimalPhraser.new
29
+ ap.source = [
30
+ { animal: "Hippo", count: 2 },
31
+ { animal: "Bird", count: 5 },
32
+ ]
33
+ ap.to_enum.to_a.should == [
34
+ { animal: "Hippo", count: 2, phrase: "There are 2 Hippos" },
35
+ { animal: "Bird", count: 5, phrase: "There are 5 Birds" },
36
+ ]
37
+ end
38
+
39
+ class Cuber < PiecePipe::AssemblyStation
40
+ def receive(inputs)
41
+ num = inputs[:num]
42
+ install squared: num * num, cubed: num * num * num
43
+ end
44
+ end
45
+
46
+ it "can install multiple keys per item" do
47
+ ap = Cuber.new
48
+ ap.source = [
49
+ { num: 2 },
50
+ { num: 5 },
51
+ ]
52
+ ap.to_enum.to_a.should == [
53
+ { num: 2, squared: 4, cubed: 8 },
54
+ { num: 5, squared: 25, cubed: 125 },
55
+ ]
56
+ end
57
+ end
58
+
59
+ context "a filtering AssemblyStation" do
60
+ class HippoHater < PiecePipe::AssemblyStation
61
+ def receive(inputs)
62
+ unless inputs[:animal] == "Hippo"
63
+ install phrase: "There are #{inputs[:count]} #{inputs[:animal]}s"
64
+ end
65
+ end
66
+ end
67
+
68
+ it "can remove elements from the work stream by NOT invoking #install for certain items" do
69
+ hh = HippoHater.new
70
+ hh.source = [
71
+ { animal: "Hippo", count: 2 },
72
+ { animal: "Bird", count: 5 },
73
+ { animal: "Hippo", count: 4 },
74
+ { animal: "Cow", count: 1 },
75
+ ]
76
+ hh.to_enum.to_a.should == [
77
+ { animal: "Bird", count: 5, phrase: "There are 5 Birds" },
78
+ { animal: "Cow", count: 1, phrase: "There are 1 Cows" },
79
+ ]
80
+ end
81
+ end
82
+
83
+ context "an exploding AssemblyStation" do
84
+ class LionCrusher < PiecePipe::AssemblyStation
85
+ def receive(inputs)
86
+ if inputs[:animal] == "Hippo"
87
+ inputs[:lion_count].times do
88
+ install injured_lion: true
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ it "can insert new elements into the work stream by invoking #install more than once for a given input" do
95
+ lc = LionCrusher.new
96
+ lc.source = [
97
+ { animal: "Hippo", lion_count: 3 }
98
+ ]
99
+ lc.to_enum.to_a.should == [
100
+ { injured_lion: true, animal: "Hippo", lion_count: 3 },
101
+ { injured_lion: true, animal: "Hippo", lion_count: 3 },
102
+ { injured_lion: true, animal: "Hippo", lion_count: 3 },
103
+ ]
104
+ end
105
+ end
106
+
107
+ context "error and oddball cases" do
108
+ context "source produces non-Hash inputs" do
109
+ class MyOtherSomething < PiecePipe::AssemblyStation
110
+ end
111
+
112
+ it "raises an error indicating requisite input type" do
113
+ as = MyOtherSomething.new
114
+ as.source = [1,2,3]
115
+ lambda do as.to_enum.to_a end.should raise_error(/AssemblyStation object MyOtherSomething.*Hash-like/)
116
+ end
117
+ end
118
+
119
+ context "overridden receive() installs nil" do
120
+ class NilInstaller < PiecePipe::AssemblyStation
121
+ def receive(inputs)
122
+ install nil
123
+ end
124
+ end
125
+ it "does a no-op" do
126
+ as = NilInstaller.new
127
+ as.source = [
128
+ {hello: "hippo"},
129
+ {hello: "lion"}
130
+ ]
131
+ as.to_enum.to_a.should == [
132
+ {hello: "hippo"},
133
+ {hello: "lion"}
134
+ ]
135
+ end
136
+ end
137
+
138
+ context "using noop()" do
139
+ class UseTheNoOp < PiecePipe::AssemblyStation
140
+ def receive(inputs)
141
+ noop
142
+ end
143
+ end
144
+ it "does a no-op" do
145
+ as = UseTheNoOp.new
146
+ as.source = [
147
+ {hello: "hippo"},
148
+ {hello: "lion"}
149
+ ]
150
+ as.to_enum.to_a.should == [
151
+ {hello: "hippo"},
152
+ {hello: "lion"}
153
+ ]
154
+ end
155
+ end
156
+
157
+ context "overwriting keys that are already present in the inputs" # not sure what that might mean?
158
+
159
+ context "accessing keys that are NOT present in the inputs" # BOOM of course.... do something more interesting?
160
+ end
161
+ end
162
+
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::Collector do
4
+ let(:key) { :the_key }
5
+ subject { described_class.new(key) }
6
+
7
+ let(:pipeline) { PiecePipe::Pipeline.new }
8
+
9
+ context "processing hashes" do
10
+ it "produces the single item pulled from incoming Hash as named by the key" do
11
+ pipeline.
12
+ source([
13
+ {one: "1", two: "2", the_key: "big"},
14
+ {one: "11", two: "22", the_key: "momma"}]).
15
+ collect(:the_key).to_enum.to_a.should == [ "big", "momma" ]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::HashedAggregator do
4
+ let(:burps_and_chirps) do
5
+ [
6
+ {key: "burp", value: 2 },
7
+ {key: "chirp", value: 2 },
8
+ {key: "chirp", value: 3 },
9
+ {key: "burp", value: 4 },
10
+ ]
11
+ end
12
+
13
+ context "default impl" do
14
+ it "produces the accumulated values for each key" do
15
+
16
+ results = PiecePipe::Pipeline.new.
17
+ source(burps_and_chirps).
18
+ step(subject).
19
+ to_a
20
+
21
+ results[0].should == { key: "burp", values: [2,4] }
22
+ results[1].should == { key: "chirp", values: [2,3] }
23
+ end
24
+
25
+ it "raises if the input items don't have :key and :value set" do
26
+ lambda do ezpipe(subject, {}).to_a end.should raise_error(/key.*value/)
27
+ lambda do ezpipe(subject, key: "ok").to_a end.should raise_error(/key.*value/)
28
+ lambda do ezpipe(subject, value: "ok").to_a end.should raise_error(/key.*value/)
29
+ end
30
+ end
31
+
32
+ context "custom #aggregate implementation" do
33
+ class Countup < PiecePipe::HashedAggregator
34
+ def aggregate(key,values)
35
+ sum = values.inject do |a,b| a+b end
36
+ produce [key, sum]
37
+ end
38
+ end
39
+
40
+ it "creates output for each key" do
41
+ results = PiecePipe::Pipeline.new.
42
+ source(burps_and_chirps).
43
+ step(Countup).
44
+ to_a
45
+ results[0].should == [ "burp", 6 ]
46
+ results[1].should == [ "chirp", 5 ]
47
+ end
48
+ end
49
+
50
+ end
51
+
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::MapStep do
4
+ context "default impl" do
5
+ it "maps the input item to itself" do
6
+ ezpipe(subject, "hello").to_a.first.should == {key: "hello", value: "hello"}
7
+ end
8
+ end
9
+
10
+ context "sample subclass of MapStep" do
11
+ class MyMapping < PiecePipe::MapStep
12
+ def map(item)
13
+ emit item[:name], item[:age]
14
+ end
15
+ end
16
+
17
+ subject do MyMapping.new end
18
+
19
+ it "can use #emit to produce Hashes w :key and :value set" do
20
+ ezpipe(subject, name: "Cosby", age: 65).to_a.first.should == {key:"Cosby", value: 65}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::MethodAssemblyStation do
4
+ let(:pipeline) { PiecePipe::Pipeline.new }
5
+
6
+ context "wrapping methods with single arguments" do
7
+ it "creates a pipeline step that 'installs' the hash returned from the Method" do
8
+ pipeline.
9
+ input(a: 1, b: 2).
10
+ assembly_step( method(:sum_a_b) ).
11
+ to_enum.to_a.should == [{a: 1, b: 2, sum: 3}]
12
+ end
13
+
14
+ it "produces unchanged if the method returns an empty hash" do
15
+ pipeline.
16
+ input(a: 1, b: 2).
17
+ assembly_step( method(:return_empty_hash) ).
18
+ to_enum.to_a.should == [{a:1, b:2}]
19
+ end
20
+
21
+ it "produces unchanged if the method returns nothing" do
22
+ pipeline.
23
+ input(a: 1, b: 2).
24
+ assembly_step( method(:return_nothing) ).
25
+ to_enum.to_a.should == [{a:1, b:2}]
26
+ end
27
+
28
+ end
29
+
30
+ #
31
+ # HELPERS
32
+ #
33
+
34
+ def sum_a_b(inputs)
35
+ { sum: inputs[:a] + inputs[:b] }
36
+ end
37
+
38
+ def return_empty_hash(inputs)
39
+ { }
40
+ end
41
+
42
+ def return_nothing(inputs)
43
+ end
44
+
45
+
46
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::MethodElement do
4
+ let(:pipeline) { PiecePipe::Pipeline.new }
5
+
6
+ context "wrapping methods with single arguments" do
7
+ it "creates a pipeline step that produces the return of the Method" do
8
+ pipeline.
9
+ input(42).
10
+ step( method(:stringify_number) ).
11
+ to_enum.to_a.should == ["!42!"]
12
+ end
13
+
14
+ it "produces nil if the method returns nothing" do
15
+ pipeline.
16
+ input(42).
17
+ step( method(:do_nothing) ).
18
+ to_enum.to_a.should == [nil]
19
+ end
20
+ end
21
+
22
+ context "wrapping methods with single arguments" do
23
+ it "creates a pipeline step wrapped around a Method, providing a 'producer' object as the second argument" do
24
+ pipeline.
25
+ input(3).
26
+ step( method(:spew_strings) ).
27
+ to_enum.to_a.should == [ ">>3", ">>3", ">>3" ]
28
+ end
29
+
30
+ it "produces NOTHING if the method doesn't invoke producer.produce" do
31
+ pipeline.
32
+ input(42).
33
+ step( method(:do_nothing2) ).
34
+ to_enum.to_a.should == []
35
+ end
36
+ end
37
+
38
+ it "raises an error if null is provided" do
39
+ lambda do pipeline.input(42).step( nil ) end.should raise_error(/nil/)
40
+ end
41
+
42
+ it "raises an error if arity is 0" do
43
+ lambda do pipeline.input(42).step( method(:no_args) ) end.should raise_error(/arguments/)
44
+ end
45
+
46
+ it "raises an error if arity greater than 2" do
47
+ lambda do pipeline.input(42).step( method(:three_args) ) end.should raise_error(/arguments.* 3/)
48
+ end
49
+
50
+ it "raises an error if arity is abitrary" do
51
+ lambda do pipeline.input(42).step( method(:many_args) ) end.should raise_error(/arguments.* -1/)
52
+ end
53
+
54
+ #
55
+ # HELPERS
56
+ #
57
+
58
+ def stringify_number(num)
59
+ "!#{num}!"
60
+ end
61
+
62
+ def do_nothing(num)
63
+ end
64
+
65
+ def spew_strings(num,producer)
66
+ num.times do
67
+ producer.produce ">>#{num}"
68
+ end
69
+ end
70
+
71
+ def do_nothing2(num,producer)
72
+ end
73
+
74
+ def no_args
75
+ "should not work"
76
+ end
77
+
78
+ def three_args(a,b,c)
79
+ "still should not work"
80
+ end
81
+
82
+ def many_args(*a)
83
+ "still should not work at all"
84
+ end
85
+
86
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::PipelineElement do
4
+
5
+ let (:array_source) { [ "hippo", "distribution", "mechanism" ] }
6
+
7
+ context "default PipelineElement" do
8
+
9
+ it "provides an enumeration of its source's elements" do
10
+ e = PiecePipe::PipelineElement.new
11
+ e.source = array_source
12
+
13
+ en = e.to_enum
14
+ en.next.should == "hippo"
15
+ en.next.should == "distribution"
16
+ en.next.should == "mechanism"
17
+ lambda do en.next end.should raise_error(StopIteration)
18
+ end
19
+ end
20
+
21
+ context "overriding #generate_sequence" do
22
+ class IntegerGenerator < PiecePipe::PipelineElement
23
+ def generate_sequence
24
+ process 1
25
+ process 2
26
+ process 3
27
+ end
28
+ end
29
+
30
+ it "can produce a sequence of items by invoking #process multiple times" do
31
+ en = IntegerGenerator.new.to_enum
32
+ en.to_a.should == [1,2,3]
33
+ end
34
+
35
+ class NothingGenerator < PiecePipe::PipelineElement
36
+ def generate_sequence
37
+
38
+ end
39
+ end
40
+
41
+
42
+ it "can produce an empty enumerator by never invoking #process" do
43
+ en = NothingGenerator.new.to_enum
44
+ en.to_a.should == []
45
+ end
46
+
47
+ context "bypassing the default #process invocation" do
48
+ class IntegerProducer < PiecePipe::PipelineElement
49
+ def generate_sequence
50
+ produce 1
51
+ produce 2
52
+ produce 3
53
+ end
54
+ end
55
+
56
+ it "can produce a sequence of items by invoking #produce multiple times" do
57
+ en = IntegerProducer.new.to_enum
58
+ en.to_a.should == [1,2,3]
59
+ end
60
+
61
+ class NothingProducer < PiecePipe::PipelineElement
62
+ def generate_sequence
63
+
64
+ end
65
+ end
66
+
67
+ it "can produce an empty enumerator by never invoking #produce" do
68
+ en = NothingProducer.new.to_enum
69
+ en.to_a.should ==[]
70
+ end
71
+ end
72
+ end
73
+
74
+ context "overriding #process" do
75
+ class StringExpander < PiecePipe::PipelineElement
76
+ def process(item)
77
+ produce "x" * item
78
+ end
79
+ end
80
+
81
+ it "can transform a sequence of items" do
82
+ se = StringExpander.new
83
+ se.source = [2,4]
84
+ se.to_enum.to_a.should == ["xx", "xxxx"]
85
+ end
86
+
87
+ class AllFilter < PiecePipe::PipelineElement
88
+ def process(item)
89
+
90
+ end
91
+ end
92
+
93
+ it "can filter items out of a sequence by neglecting to call #produce" do
94
+ al = AllFilter.new
95
+ al.source = [3,6]
96
+ al.to_enum.to_a.should == []
97
+ end
98
+
99
+ class Exploder < PiecePipe::PipelineElement
100
+ def process(item)
101
+ item.times do
102
+ produce "hi #{item}"
103
+ end
104
+ end
105
+ end
106
+
107
+ it "can inflate a sequence by invoking #produce multiple times per item" do
108
+ ex = Exploder.new
109
+ ex.source = [2,3]
110
+ ex.to_enum.to_a.should == ["hi 2", "hi 2", "hi 3", "hi 3", "hi 3"]
111
+ end
112
+ end
113
+
114
+ context "nill source" do
115
+ class MySomething < PiecePipe::PipelineElement
116
+ end
117
+
118
+ it "raises an error" do
119
+ e = MySomething.new
120
+ e.source = nil
121
+ lambda do e.to_enum.to_a end.should raise_error(/source.*MySomething.*is nil/)
122
+ end
123
+ end
124
+
125
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ describe PiecePipe::Pipeline do
4
+ class OneTwoThree < PiecePipe::PipelineElement
5
+ def generate_sequence
6
+ produce 1
7
+ produce 2
8
+ produce 3
9
+ end
10
+ end
11
+
12
+ class TheDoubler < PiecePipe::PipelineElement
13
+ def process(item)
14
+ produce item * 2
15
+ end
16
+ end
17
+
18
+ context "super simple" do
19
+ it "produces the values of its source" do
20
+ subject.step(OneTwoThree)
21
+ subject.to_enum.to_a.should == [ 1, 2, 3]
22
+ end
23
+ end
24
+
25
+ context "two steps" do
26
+ it "produces transformed values by processing both steps for each item" do
27
+ subject.
28
+ step(OneTwoThree).
29
+ step(TheDoubler)
30
+
31
+ subject.to_enum.to_a.should == [ 2, 4, 6]
32
+ end
33
+ end
34
+
35
+ context "using instantiated PipelineElements as steps" do
36
+ it "produces transformed values by processing both steps for each item" do
37
+ subject.
38
+ step(OneTwoThree.new).
39
+ step(TheDoubler.new)
40
+
41
+ subject.to_enum.to_a.should == [ 2, 4, 6]
42
+ end
43
+ end
44
+
45
+ context "using an enumerable object as the pipeline source" do
46
+ it "works with Arrays" do
47
+ subject.
48
+ source([10,20,30]).
49
+ step(TheDoubler)
50
+ subject.to_enum.to_a.should == [20, 40, 60]
51
+ end
52
+
53
+ it "works with PipelineElements" do
54
+ subject.
55
+ source(OneTwoThree.new).
56
+ step(TheDoubler)
57
+ subject.to_enum.to_a.should == [2, 4, 6]
58
+ end
59
+
60
+ it "raises if there's already something set as a source" do
61
+ subject.source([10,20,30])
62
+ lambda do subject.source([1,2,3]) end.should raise_error(/source already set/i)
63
+ end
64
+
65
+ it "raises if there's already a step" do
66
+ subject.step(OneTwoThree)
67
+ lambda do subject.source([1,2,3]) end.should raise_error(/source already set/i)
68
+ end
69
+ end
70
+
71
+ describe "#collect" do
72
+ let(:project_info) {
73
+ [
74
+ {name: "Project 1", project_health_summary: "Summary 1"},
75
+ {name: "Project 2", project_health_summary: "Summary 2"},
76
+ ]
77
+ }
78
+
79
+ it "maps the pipeline items down to the values indicated by the given key" do
80
+ subject.
81
+ source(project_info).
82
+ collect(:project_health_summary)
83
+ subject.to_enum.to_a.should == [
84
+ "Summary 1",
85
+ "Summary 2"
86
+ ]
87
+ end
88
+ end
89
+
90
+ end
@@ -0,0 +1,97 @@
1
+ require 'piece_pipe'
2
+
3
+ module SpecHelpers
4
+ def ezpipe(single_step, inputz)
5
+ PiecePipe::Pipeline.new.
6
+ input(inputz).
7
+ step(single_step)
8
+ end
9
+ end
10
+
11
+ RSpec.configure do |config|
12
+ config.mock_with :mocha
13
+ config.include SpecHelpers
14
+ end
15
+
16
+ class NilClass
17
+ def stubs(*args)
18
+ raise "Tried to stub a method on NilClass with arguments: #{args.inspect}. Are you using a non-existant mock instance?"
19
+ end
20
+
21
+ def expects(*args)
22
+ raise "Tried to expect a method on NilClass with arguments: #{args.inspect}. Are you using a non-existant mock instance?"
23
+ end
24
+
25
+ def to_regexp
26
+ # match empty string or &nbsp;
27
+ /^$|&nbsp;/
28
+ end
29
+ end
30
+
31
+ def clever_girl
32
+ pending <<-EOVR
33
+ __ ______
34
+ ,^.__.>--"~~'_.--~_)~^.
35
+ _L^~ ~ (~ _.-~ \\. |\\
36
+ CLEVER GIRL.... ,-~ __ __,^"/\\_A_/ /' \\
37
+ / ,-" "~~" _) \\ ~_,^ /\\
38
+ // / ,-~\\ x~" \\._"-~ ~ _Y
39
+ Y' Y. (__.// / " , "\\_r ' ]
40
+ J-._l_>---r{ ~ \\__/ \\ _/
41
+ (_ ( (~ ( ~"--- _.-~ `\\ / \\ !
42
+ (_"~--^----^--------" _.-c Y /Y'
43
+ l~--v---.,____.--" / !_/ |
44
+ \\.__!.__./~-. _/ / \\ !
45
+ `x.\\_____\\_,>---"~___Y\\__/Y'
46
+ ~ ~(~~()"~___)/ /\\|
47
+ (~~ ~~__) \\_t
48
+ (~~ ~~__)\\_/ |
49
+ (~~ ~~__)\\_/ |
50
+ { ~~ ~~ }/ \\
51
+ EOVR
52
+ end
53
+
54
+ def pending_tiger_no_dragon
55
+ pending <<-EOT
56
+
57
+ ,''',
58
+ .' ., .', ../'''',
59
+ .'. %%, %.', .,/' .,% :
60
+ .'.% %%%,`%%%'. .....,,,,,,..... .,%%% .,%%'. .'
61
+ : %%% %%%%%%',:%%>>%>' .,>>%>>%>%>>%%>,. `%%%',% :
62
+ : %%%%%%%'.,>>>%' .,%>%>%'.,>%>%' . `%>>>,. `%%%:'
63
+ ` %%%%'.,>>%' .,%>>%>%' .,%>%>%' .>>%,. `%%>>,. `%
64
+ `%'.,>>>%'.,%%%%%%%' .,%%>%%>%' >>%%>%>>%.`%% %% `,
65
+ ,`%% %%>>>%%%>>%%>%%>>%>>%>%%% %%>%%>%%>>%>%%%' % %,
66
+ ,%>%'.>>%>%'%>>%%>%%%%>%' `%>%>>%%.`%>>%.
67
+ ,%%>' .>%>%'.%>%>>%%%>>%' ,%%>>%%>%>>%>>%>%%,.`%%%>%%. `%>%.
68
+ ` ,%' .>%%%'.%>%>>%' .,%%%%%%%%' `%%%%%%.`%%>%% .%%>
69
+ .%>% .%%>' :%>>%%'.,%%%%%%%%%'.%%%%%' `%%%%.`%%%%%.%%%%> %%>%.
70
+ ,%>%' >>%% >%' `%%%%' `%%%%%%%'.,>,. `%%%%' `%%%>>%%>%
71
+ .%%>%' .%%>' %>>%, %% oO ~ Oo %%%>>'.>>>>>>. `% oO ~ Oo'.%%%'%>%,
72
+ %>'%> .%>%>% %%>%%%' `OoooO'.%%>>'.>>>%>>%>>.`%`OoooO'.%%>% '%>%
73
+ %',%' %>%>%' %>%>%>% .%,>,>, `>'.>>%>%%>>>%>.`%,>,>' %%%%> .>%>,
74
+ ` %>% `%>>%%. `%%% %' >%%%%%%>, ' >>%>>%%%>%>>> >>%%' ,%%>%'.%%>>%.
75
+ .%%' %%%%>%. `>%%. %>%%>>>%.>> >>>%>%%%%>%>>.>>>'.>%>%>' %>>%>%%
76
+ `.%% `%>>%%> %%>% %>>>%%%>>'.>%>>>>%%%>>%>>.>',%>>%' ,>%'>% '
77
+ %>' %%%%%%' `%%' %%%%%> >' >>>>%>>%%>>%>>%> %%>%>' .%>%% .%%
78
+ %>%>, %>%%>>%%, %>%>% `%% %>> >>>%>>>%%>>>>%>> %%>>,%>%%'.%>%,
79
+ %>%>%%, `%>%%>%>%, %>%%> ,%>%>>>.>>`.,. `" ..'>.%. % %>%>%'.%>%%;
80
+ %'`%%>% %%>%% %>% %'.>%>>%>%%>>%::. `, /' ,%>>>%>. >%>%'.%>%'%'
81
+ ` .%>%' >%%% %>%%'.>%>%;''.,>>%%>%%::. ..'.,%>>%>%>,`% %'.>%%' '
82
+ %>%>%% `%> >%%'.%%>%>>%>%>%>>>%>%>>%,,::,%>>%%>%>>%>%% `>>%>'
83
+ %'`%%>%>>% %>'.%>>%>%>>;'' ..,,%>%>%%/::%>%%>>%%,,.``% .%>%%
84
+ ` `%>%>>%%' %>%%>>%>>%>%>%>%%>%/' `%>%%>%>>%%% ' .%'
85
+ %' `%>% `%>%%;'' .,>>%>%/',;;;;;,;;;;,`%>%>%,`%' '
86
+ ` ` ` `%>%%%>%%>%%;/ @a;;;;;;;;;;;a@ >%>%%'
87
+ `/////////';, `@a@@a@@a@@aa@',;`//'
88
+ `//////.;;,,............,,;;//'
89
+ `////;;;;;;;;;;;;;;;;;/'
90
+ `/////////////////'
91
+ /
92
+ /
93
+ /
94
+ HAS TO COME CORRECT!!!!!
95
+ EOT
96
+ end
97
+
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: piece_pipe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Crosby
9
+ - Shawn Anderson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-30 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mocha
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rspec
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ type: :development
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
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
+ description: ! 'PiecePipe is about breaking your problem into its smallest, most interesting
64
+ pieces, solving those pieces and not spending time on the glue code between them. '
65
+ email:
66
+ - crosby@atomicobject.com
67
+ - shawn42@gmail.com
68
+ executables: []
69
+ extensions: []
70
+ extra_rdoc_files: []
71
+ files:
72
+ - .gitignore
73
+ - Gemfile
74
+ - LICENSE
75
+ - README.md
76
+ - Rakefile
77
+ - lib/piece_pipe.rb
78
+ - lib/piece_pipe/assembly_station.rb
79
+ - lib/piece_pipe/collector.rb
80
+ - lib/piece_pipe/debug_step.rb
81
+ - lib/piece_pipe/hashed_aggregator.rb
82
+ - lib/piece_pipe/map_step.rb
83
+ - lib/piece_pipe/method_assembly_station.rb
84
+ - lib/piece_pipe/method_element.rb
85
+ - lib/piece_pipe/pipeline.rb
86
+ - lib/piece_pipe/pipeline_element.rb
87
+ - lib/piece_pipe/tap_step.rb
88
+ - lib/piece_pipe/version.rb
89
+ - piece_pipe.gemspec
90
+ - spec/assembly_station_spec.rb
91
+ - spec/collector_spec.rb
92
+ - spec/hashed_aggregator_spec.rb
93
+ - spec/map_step_spec.rb
94
+ - spec/method_assembly_station_spec.rb
95
+ - spec/method_element_spec.rb
96
+ - spec/pipeline_element_spec.rb
97
+ - spec/pipeline_spec.rb
98
+ - spec/spec_helper.rb
99
+ homepage: http://atomicobject.com
100
+ licenses: []
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ none: false
113
+ requirements:
114
+ - - ! '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.24
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: PiecePipe helps you break your code into small interesting pieces and provides
123
+ the glue for pipelining them together to provide elegant, readable code.
124
+ test_files:
125
+ - spec/assembly_station_spec.rb
126
+ - spec/collector_spec.rb
127
+ - spec/hashed_aggregator_spec.rb
128
+ - spec/map_step_spec.rb
129
+ - spec/method_assembly_station_spec.rb
130
+ - spec/method_element_spec.rb
131
+ - spec/pipeline_element_spec.rb
132
+ - spec/pipeline_spec.rb
133
+ - spec/spec_helper.rb
134
+ has_rdoc: