linepipe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .ruby-version
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in linepipe.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Wimdu GmbH
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,124 @@
1
+ # Linepipe
2
+
3
+ A tool to aid in processing data in a pipeline, making every step easily
4
+ testable and benchmarkable.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'linepipe'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install linepipe
19
+
20
+ ## Usage
21
+
22
+ Linepipe's DSL consists of 4 different parts:
23
+
24
+ * `setup`: Optional setup that will be run at the beginning.
25
+ * `data`: The input data.
26
+ * `process`: As many of these as you want will conform the steps of your
27
+ algorithm.
28
+ * `expect`: In development mode, each of these will be run against your final
29
+ output data to ensure its conformity with your expectations.
30
+
31
+ While developing a processing algorithm, `Linepipe.develop` is your friend. Each
32
+ `process` block will be reduced against your data in order, and then each
33
+ `expect` block will be run against the final output to ensure that it works.
34
+
35
+ ```ruby
36
+ linepipe = Linepipe.run do
37
+ data {
38
+ %w(foo bar baz)
39
+ }
40
+
41
+ process("Upcasing") { |data|
42
+ data.map(&:upcase)
43
+ }
44
+
45
+ process("Reversing") { |data|
46
+ data.reverse
47
+ }
48
+
49
+ expect { |data|
50
+ data == %w(BAZ BAR FOO)
51
+ }
52
+ end
53
+
54
+ linepipe.result # => %W(BAZ BAR FOO)
55
+ ```
56
+
57
+ Once you're comfortable with your algorithm, just change your call to
58
+ `Linepipe.develop` to `Linepipe.run` and no expectations will be run.
59
+
60
+ ## Testing your linepipes
61
+
62
+ `Linepipe.run`, `Linepipe.benchmark` and `Linepipe.develop` return a `Linepipe::Process` object that
63
+ responds to two important methods: `output` and a hash-like
64
+ interface to access each step. In our case above we would access the second step
65
+ "Reversing" (from a test or wherever) like this:
66
+
67
+ ```ruby
68
+ step = linepipe["Reversing"]
69
+ # => #<Step ...>
70
+ expect(step.apply([1,2,3])).to eq([3,2,1])
71
+ # => [3,2,1]
72
+ ```
73
+
74
+ This way you can test every stage of your linepipe separately against as many
75
+ inputs as you want.
76
+
77
+ ## Benchmarking your linepipes
78
+
79
+ To switch Linepipe into benchmark mode, just call `Linepipe.benchmark` instead
80
+ of `.develop` or `.run`. This will print a detailed benchmark for every step of
81
+ your algorithm so you can easily identify and fix bottlenecks.
82
+
83
+ ```ruby
84
+ linepipe = Linepipe.benchmark(10_000) do
85
+ data {
86
+ %w(foo bar baz)
87
+ }
88
+
89
+ process("Upcasing") { |data|
90
+ data.map(&:upcase)
91
+ }
92
+
93
+ process("Reversing") { |data|
94
+ data.reverse
95
+ }
96
+
97
+ expect { |data|
98
+ data == %w(BAZ BAR FOO)
99
+ }
100
+ end
101
+ ```
102
+
103
+ Will output to the screen:
104
+
105
+ Rehearsal ---------------------------------------------
106
+ Upcasing 0.020000 0.000000 0.020000 ( 0.024458)
107
+ Reversing 0.000000 0.000000 0.000000 ( 0.004000)
108
+ ------------------------------------ total: 0.020000sec
109
+
110
+ user system total real
111
+ Upcasing 0.020000 0.000000 0.020000 ( 0.022565)
112
+ Reversing 0.010000 0.000000 0.010000 ( 0.007034)
113
+
114
+ ## Contributing
115
+
116
+ 1. Fork it
117
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
118
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
119
+ 4. Push to the branch (`git push origin my-new-feature`)
120
+ 5. Create new Pull Request
121
+
122
+ ## License
123
+
124
+ Copyright (c) 2013 Wimdu GmbH (MIT License). See LICENSE.txt for details.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "bundler/setup"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new do |t|
6
+ t.rspec_opts = ["--color", '--format doc', '--order rand']
7
+ end
8
+ task :default => :spec
@@ -0,0 +1,23 @@
1
+ require_relative '../lib/linepipe'
2
+
3
+ Linepipe.benchmark(100_000) do
4
+ data {
5
+ %w(foo bar baz)
6
+ }
7
+
8
+ step("Upcasing") { |data|
9
+ data.map(&:upcase)
10
+ }
11
+
12
+ step("Reversing") { |data|
13
+ data.reverse
14
+ }
15
+
16
+ step("Sorting") { |data|
17
+ data.sort
18
+ }
19
+
20
+ expect { |data|
21
+ data == %w(BAZ BAR FOO)
22
+ }
23
+ end
@@ -0,0 +1,40 @@
1
+ require_relative '../lib/linepipe'
2
+ require 'minitest/autorun'
3
+
4
+ class DevelopTest < MiniTest::Unit::TestCase
5
+ def linepipe
6
+ @linepipe ||= Linepipe.develop do
7
+ data {
8
+ %w(foo bar baz)
9
+ }
10
+
11
+ step("Upcasing") { |data|
12
+ data.map(&:upcase)
13
+ }
14
+
15
+ step("Reversing") { |data|
16
+ data.reverse
17
+ }
18
+
19
+ step("Sorting") { |data|
20
+ data.sort
21
+ }
22
+
23
+ expect { |data|
24
+ data == %w(BAR BAZ FOO)
25
+ }
26
+ end
27
+ end
28
+
29
+ def test_upcasing
30
+ assert_equal %w(A B), linepipe['Upcasing'].apply(%w(a b))
31
+ end
32
+
33
+ def test_reversing
34
+ assert_equal %w(b a), linepipe['Reversing'].apply(%w(a b))
35
+ end
36
+
37
+ def test_sorting
38
+ assert_equal %w(a b c), linepipe['Sorting'].apply(%w(c a b))
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "expectation"
2
+ require_relative "step"
3
+
4
+ module Linepipe
5
+ module DSL
6
+ def setup(&block)
7
+ @setup = block
8
+ end
9
+
10
+ def data(data=nil, &block)
11
+ @data = data ? -> { data } : block
12
+ end
13
+
14
+ def step(name=nil, &block)
15
+ @steps << Step.new(name, &block)
16
+ end
17
+
18
+ def expect(msg=nil, &block)
19
+ @expectations << Expectation.new(msg, io, &block)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Linepipe
2
+ class Expectation
3
+ def initialize(msg="Assertion failed", io=STDOUT, &block)
4
+ @msg = msg
5
+ @io = io
6
+ @block = block
7
+ end
8
+
9
+ def successful?(data)
10
+ if !block.call(data)
11
+ io.puts "Expectation failed at #{block.source_location.join(':')} (#{msg})"
12
+ return false
13
+ end
14
+ true
15
+ end
16
+
17
+ private
18
+ attr_reader :block, :msg, :io
19
+ end
20
+ end
21
+
@@ -0,0 +1,83 @@
1
+ require_relative "dsl"
2
+
3
+ module Linepipe
4
+ class Process
5
+ include DSL
6
+ attr_reader :output, :steps
7
+
8
+ def initialize(io=STDOUT)
9
+ @data = []
10
+ @steps = []
11
+ @setup = nil
12
+ @output = nil
13
+ @expectations = []
14
+ @io = io
15
+ end
16
+
17
+ def [](name)
18
+ steps.detect { |s| s.name == name }
19
+ end
20
+
21
+ def run
22
+ run_setup
23
+ @output = steps.reduce(initial_data) { |d, step| step.apply(d) }
24
+ end
25
+
26
+ def develop
27
+ run_setup
28
+ @output = steps.to_enum.with_index.reduce(initial_data) { |d, (step, idx)|
29
+ io.puts "Stage #{idx} #{step.name}"
30
+ io.puts "Input: #{d}"
31
+ step.apply(d).tap do |r|
32
+ io.puts "Output: #{r}"
33
+ end
34
+ }
35
+
36
+ if expectations.all? { |exp| exp.successful?(output) }
37
+ io.puts "SUCCESS!"
38
+ end
39
+ end
40
+
41
+ def benchmark(iterations)
42
+ require 'benchmark'
43
+ require 'stringio'
44
+
45
+ run_setup
46
+
47
+ label_length = steps.map(&:name).map(&:length).max
48
+
49
+ out = $stdout
50
+ $stdout = stringio = StringIO.new
51
+
52
+ Benchmark.bmbm(label_length) do |x|
53
+ @output = steps.reduce(initial_data) { |d, step|
54
+ result = step.apply(d)
55
+ x.report(step.name) { iterations.times { step.apply(d) } }
56
+ result
57
+ }
58
+ end
59
+
60
+ io.puts stringio.string
61
+ ensure
62
+ $stdout = out
63
+ end
64
+
65
+
66
+ private
67
+ attr_reader :expectations, :io
68
+
69
+ def run_setup
70
+ @setup.call if @setup
71
+ end
72
+
73
+ def initial_data
74
+ if @data.is_a?(Proc)
75
+ @data.call
76
+ else
77
+ puts "[Linepipe] Warn: You need to specify an initial data set"
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
@@ -0,0 +1,18 @@
1
+ module Linepipe
2
+ class Step
3
+ attr_reader :name
4
+
5
+ def initialize(name=nil, &block)
6
+ @name = name
7
+ @block = block
8
+ end
9
+
10
+ def apply(data)
11
+ block.call(data)
12
+ end
13
+
14
+ private
15
+ attr_reader :block
16
+ end
17
+ end
18
+
@@ -0,0 +1,3 @@
1
+ module Linepipe
2
+ VERSION = "0.1.0"
3
+ end
data/lib/linepipe.rb ADDED
@@ -0,0 +1,29 @@
1
+ require_relative "linepipe/version"
2
+ require_relative "linepipe/process"
3
+
4
+ module Linepipe
5
+ class << self
6
+
7
+ def develop(&block)
8
+ build_process(block) { |process| process.develop }
9
+ end
10
+
11
+ def run(&block)
12
+ build_process(block) { |process| process.run }
13
+ end
14
+
15
+ def benchmark(iterations, &block)
16
+ build_process(block) { |process| process.benchmark(iterations) }
17
+ end
18
+
19
+
20
+ private
21
+
22
+ def build_process(dsl_block, &block)
23
+ Process.new.tap do |process|
24
+ process.instance_eval(&dsl_block)
25
+ yield process
26
+ end
27
+ end
28
+ end
29
+ end
data/linepipe.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'linepipe/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "linepipe"
8
+ gem.version = Linepipe::VERSION
9
+ gem.authors = ["Josep M. Bach"]
10
+ gem.email = ["josep.bach@wimdu.com"]
11
+ gem.description = %q{Process data one step at a time.}
12
+ gem.summary = %q{Process data one step at a time.}
13
+ gem.homepage = "https://github.com/wimdu/linepipe"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency 'rspec'
21
+ end
@@ -0,0 +1,35 @@
1
+ require 'rspec'
2
+ require 'linepipe'
3
+ require 'stringio'
4
+
5
+ module Linepipe
6
+ describe Expectation, '#successful?' do
7
+ let(:io) { StringIO.new }
8
+
9
+ describe 'when it fails' do
10
+ let(:expectation) do
11
+ Expectation.new('Failure message', io) { false }
12
+ end
13
+
14
+ it 'prints the message to the output' do
15
+ expectation.successful?(%w(some data))
16
+ expect(io.string).to match(/expectation_spec/)
17
+ expect(io.string).to match(/Failure message/)
18
+ end
19
+
20
+ it 'returns false' do
21
+ expect(expectation.successful?(%w(some data))).to be_false
22
+ end
23
+ end
24
+
25
+ describe 'when it passes' do
26
+ let(:expectation) do
27
+ Expectation.new('Failure message', io) { true }
28
+ end
29
+
30
+ it 'returns true' do
31
+ expect(expectation.successful?(%w(some data))).to be_true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,99 @@
1
+ require 'rspec'
2
+ require 'linepipe'
3
+ require 'stringio'
4
+
5
+ module Linepipe
6
+ describe Process do
7
+ let(:io) { StringIO.new }
8
+
9
+ let(:process) do
10
+ Process.new(io).tap do |process|
11
+ process.setup { process.taint }
12
+ process.data { %w(foo bar baz) }
13
+ process.step('Upcasing') { |data| data.map(&:upcase) }
14
+ process.step('Reversing', &:reverse)
15
+ process.expect { |data| data.first == 'BAZ' }
16
+ end
17
+ end
18
+
19
+ describe '#[]' do
20
+ it 'gets a step' do
21
+ step = process['Upcasing']
22
+ expect(step.apply(['blah'])).to eq(['BLAH'])
23
+ end
24
+ end
25
+
26
+ describe '#run' do
27
+ before do
28
+ process.run
29
+ end
30
+
31
+ it 'runs the setup' do
32
+ expect(process).to be_tainted
33
+ end
34
+
35
+ it 'assigns the output' do
36
+ expect(process.output).to eq(%w(BAZ BAR FOO))
37
+ end
38
+ end
39
+
40
+ describe '#develop' do
41
+ it 'runs the setup' do
42
+ process.develop
43
+ expect(process).to be_tainted
44
+ end
45
+
46
+ it 'assigns the output' do
47
+ process.develop
48
+ expect(process.output).to eq(%w(BAZ BAR FOO))
49
+ end
50
+
51
+ it 'outputs information to the io stream' do
52
+ process.develop
53
+ expect(io.string).to match(/Stage 0 Upcasing/)
54
+ expect(io.string).to match(/Stage 1 Reversing/)
55
+ end
56
+
57
+ describe 'when the expectations pass' do
58
+ it 'outputs SUCCESS' do
59
+ process.develop
60
+ expect(io.string).to match(/SUCCESS/)
61
+ end
62
+ end
63
+
64
+ describe 'when the expectations fail' do
65
+ before do
66
+ process.expect('is not a number!') { |data| data.kind_of?(Numeric) }
67
+ process.develop
68
+ end
69
+
70
+ it 'does not output SUCCESS' do
71
+ expect(io.string).to_not match(/SUCCESS/)
72
+ end
73
+
74
+ it 'outputs errors' do
75
+ expect(io.string).to match(/is not a number!/)
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#benchmark' do
81
+ before do
82
+ process.benchmark(2)
83
+ end
84
+
85
+ it 'runs the setup' do
86
+ expect(process).to be_tainted
87
+ end
88
+
89
+ it 'assigns the output' do
90
+ expect(process.output).to eq(%w(BAZ BAR FOO))
91
+ end
92
+
93
+ it 'outputs the benchmark results' do
94
+ expect(io.string).to match(/real/)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
@@ -0,0 +1,17 @@
1
+ require 'rspec'
2
+ require 'linepipe'
3
+
4
+ module Linepipe
5
+ describe Step, '#apply' do
6
+ let(:step) { Step.new('upcase', &:upcase) }
7
+
8
+ it 'exposes a name' do
9
+ expect(step.name).to eq('upcase')
10
+ end
11
+
12
+ it 'calls the block with the data' do
13
+ expect(step.apply('data')).to eq('DATA')
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,40 @@
1
+ require 'rspec'
2
+ require 'linepipe'
3
+
4
+ describe Linepipe do
5
+ describe '.develop' do
6
+ it 'creates a new linepipe process in development mode' do
7
+ process = Linepipe::Process.new
8
+ Linepipe::Process.stub(:new).and_return process
9
+
10
+ process.should_receive(:instance_eval)
11
+ process.should_receive(:develop)
12
+
13
+ expect(Linepipe.develop {}).to eq(process)
14
+ end
15
+ end
16
+
17
+ describe '.run' do
18
+ it 'creates a new linepipe process in run mode' do
19
+ process = Linepipe::Process.new
20
+ Linepipe::Process.stub(:new).and_return process
21
+
22
+ process.should_receive(:instance_eval)
23
+ process.should_receive(:run)
24
+
25
+ expect(Linepipe.run {}).to eq(process)
26
+ end
27
+ end
28
+
29
+ describe '.benchmark' do
30
+ it 'creates a new linepipe process in benchmark mode' do
31
+ process = Linepipe::Process.new
32
+ Linepipe::Process.stub(:new).and_return process
33
+
34
+ process.should_receive(:instance_eval)
35
+ process.should_receive(:benchmark).with(2)
36
+
37
+ expect(Linepipe.benchmark(2) {}).to eq(process)
38
+ end
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linepipe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josep M. Bach
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Process data one step at a time.
31
+ email:
32
+ - josep.bach@wimdu.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - examples/benchmark.rb
43
+ - examples/develop.rb
44
+ - lib/linepipe.rb
45
+ - lib/linepipe/dsl.rb
46
+ - lib/linepipe/expectation.rb
47
+ - lib/linepipe/process.rb
48
+ - lib/linepipe/step.rb
49
+ - lib/linepipe/version.rb
50
+ - linepipe.gemspec
51
+ - spec/pipeline/expectation_spec.rb
52
+ - spec/pipeline/process_spec.rb
53
+ - spec/pipeline/step_spec.rb
54
+ - spec/pipeline_spec.rb
55
+ homepage: https://github.com/wimdu/linepipe
56
+ licenses: []
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 1.8.23
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Process data one step at a time.
79
+ test_files:
80
+ - spec/pipeline/expectation_spec.rb
81
+ - spec/pipeline/process_spec.rb
82
+ - spec/pipeline/step_spec.rb
83
+ - spec/pipeline_spec.rb