newbamboo-proto_processor 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +150 -0
- data/Rakefile +48 -0
- data/VERSION.yml +4 -0
- data/lib/proto_processor.rb +16 -0
- data/lib/proto_processor/report.rb +77 -0
- data/lib/proto_processor/strategy.rb +51 -0
- data/lib/proto_processor/task.rb +158 -0
- data/lib/proto_processor/task_runner.rb +24 -0
- data/spec/base_strategy_spec.rb +112 -0
- data/spec/base_task_spec.rb +181 -0
- data/spec/report_spec.rb +9 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/task_validations_spec.rb +55 -0
- data/spec/tasks_runner_spec.rb +80 -0
- metadata +73 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Ismael Celis
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
## proto_processor
|
2
|
+
|
3
|
+
A couple of modules to ease the creation of background processors or any kind of delegated task or tasks for which you want flow control and error reporting.
|
4
|
+
|
5
|
+
## Install
|
6
|
+
|
7
|
+
sudo gem install ismasan-proto_processor
|
8
|
+
|
9
|
+
## Examples
|
10
|
+
|
11
|
+
### Strategies
|
12
|
+
|
13
|
+
You start by defining a *strategy*. A strategy defines the sequence of tasks your program will carry out.
|
14
|
+
|
15
|
+
require 'rubygems'
|
16
|
+
require 'proto_processor'
|
17
|
+
|
18
|
+
class ImageStrategy
|
19
|
+
include ProtoProcessor::Strategy
|
20
|
+
|
21
|
+
# :process runs you strategy
|
22
|
+
#
|
23
|
+
def process
|
24
|
+
|
25
|
+
options = {:width => 300, :height => 200}
|
26
|
+
|
27
|
+
# set the initial input
|
28
|
+
with_input File.open('some_file.jpg')
|
29
|
+
|
30
|
+
# Run the file though a resize task
|
31
|
+
run_task ResizeTask, options
|
32
|
+
|
33
|
+
# store the modified file
|
34
|
+
run_task StoreFileTask
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
Run it:
|
40
|
+
|
41
|
+
strategy = ImageStrategy.new
|
42
|
+
report = strategy.run
|
43
|
+
puts report.inspect
|
44
|
+
|
45
|
+
The Strategy module just adds a few methods to run your tasks in a declarative manner. The :process method sets an input to be used as a starting point with *with_input*, and declares the sequence of tasks to be run on that input with *run_task*, which takes the task class and an options hash as arguments.
|
46
|
+
|
47
|
+
Strategy#run captures the output of tasks and gives you a Report object, with information about the tasks run, their output and possible errors.
|
48
|
+
|
49
|
+
Apart from the *process* method, Strategies are normal Ruby classes so you can go ahead and add whatever functionality you want to them, including, for example, an *initialize* method, or a factory.
|
50
|
+
|
51
|
+
### Tasks
|
52
|
+
|
53
|
+
Tasks do the real work. They also implement a *process* method where you can put you image resizing, file storing, email sending or whatever code you want.
|
54
|
+
|
55
|
+
require 'mini_magick'
|
56
|
+
class ResizeTask
|
57
|
+
include ProtoProcessor::Task
|
58
|
+
|
59
|
+
expects_options_with :width
|
60
|
+
expects_options_with :height
|
61
|
+
|
62
|
+
def process
|
63
|
+
image = MiniMagick::Image.new(input.path)
|
64
|
+
image.resize "#{options[:width]}x#{options[:height]}"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
That's pretty much it. Any exceptions raised within *process* will be captured, logged and stored in the task report which will be available as part of the Strategy-wide Report object.
|
69
|
+
|
70
|
+
Every task has an *options* hash available.
|
71
|
+
|
72
|
+
### Validating task options
|
73
|
+
|
74
|
+
The previous example shows a simple way of checking that a task was passed required parameters as part of the options hash.
|
75
|
+
|
76
|
+
expects_options_with :width
|
77
|
+
|
78
|
+
If parameters declared in this way are not present in the options, a ProtoProcessor::MissingParametersError exception will be raised and logged in the task report (your task won't blow up though). The process method won't be run.
|
79
|
+
|
80
|
+
You can also raise manually in the process method.
|
81
|
+
|
82
|
+
def process
|
83
|
+
raise ArgumentError, ":width option must be > 0" if options[:width] < 1
|
84
|
+
end
|
85
|
+
|
86
|
+
Tasks also have *before_process* and *after_process* callbacks that will run if defined. You can guess what they do :)
|
87
|
+
|
88
|
+
Lastly, you can define your own *validate* method which will be run before processing. If validate returns false, the task won't process and I ProtoProcessor::TaskInvalidError will be logged in the error report
|
89
|
+
|
90
|
+
def validate
|
91
|
+
(1..500).include? options[:width]
|
92
|
+
end
|
93
|
+
|
94
|
+
### Chaining tasks in strategies
|
95
|
+
|
96
|
+
run_task [CropTask, ResizeTask, ZipTask, EmailTask], options
|
97
|
+
|
98
|
+
Tasks can be chained and run sequentially as a unit. The input, options and report will be passed down the chain to the last task. The final, composed output and report is then returned to the main strategy and available in the strategy Report.
|
99
|
+
|
100
|
+
You can use task chains to process elements in a collection:
|
101
|
+
|
102
|
+
BIG = {:width => 500, :height => 500}
|
103
|
+
MEDIUM = {:width => 200, :height => 200}
|
104
|
+
SMALL = {:width => 100, :height => 100}
|
105
|
+
|
106
|
+
with_input some_file_here
|
107
|
+
|
108
|
+
# Crop the original
|
109
|
+
run_task CropTask, {:square => true}
|
110
|
+
|
111
|
+
# Produce resized versions and store them somewhere
|
112
|
+
[BIG, MEDIUM, SMALL].each do |dimensions|
|
113
|
+
run_task [ResizeTask, ZipTask, StorageTask], dimensions
|
114
|
+
end
|
115
|
+
|
116
|
+
If any task in the chain fails the error will be logged. The following tasks will not be processed.
|
117
|
+
|
118
|
+
### Stand alone tasks
|
119
|
+
|
120
|
+
Tasks are quite simple objects. They expect an array with an input, an options hash and a report hash as an argument and return the same.
|
121
|
+
|
122
|
+
resize = ResizeTask.new([some_file, {:width => 100, :height => 100}, {}])
|
123
|
+
resize.run # => [input, options, report]
|
124
|
+
|
125
|
+
The task report (just a hash) is populated by tasks and passed along to the next task in the chain, if any. This is a good place to put data resulting from your processing that you want to make available for the next task. You do this with the *report!* shortcut within the process method or any other method you define in your task.
|
126
|
+
|
127
|
+
# ... do some processing
|
128
|
+
report! :some_key, 'Some value'
|
129
|
+
|
130
|
+
If a task expects a certain key in the report passed from a previous task in a chain, you can make it explicit just like with options:
|
131
|
+
|
132
|
+
expects_report_with :some_key
|
133
|
+
|
134
|
+
If :some_key doesn't exist in the passed report, the task will not process and halt any chain it is in.
|
135
|
+
|
136
|
+
### Logging
|
137
|
+
|
138
|
+
Default to STDOUT. Just add your own logger and logger level.
|
139
|
+
|
140
|
+
ProtoProcessor.logger = Logger.new('my_processor.log')
|
141
|
+
ProtoProcessor.logger.level = Logger::ERROR
|
142
|
+
|
143
|
+
## TODO
|
144
|
+
|
145
|
+
* Improve DSL in strategies
|
146
|
+
* Better log formatting?
|
147
|
+
|
148
|
+
## Copyright
|
149
|
+
|
150
|
+
Copyright (c) 2009 Ismael Celis. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "proto_processor"
|
8
|
+
gem.summary = %Q{TODO}
|
9
|
+
gem.email = "ismaelct@gmail.com"
|
10
|
+
gem.homepage = "http://github.com/ismasan/proto_processor"
|
11
|
+
gem.authors = ["Ismael Celis"]
|
12
|
+
|
13
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'spec/rake/spectask'
|
20
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
21
|
+
spec.libs << 'lib' << 'spec'
|
22
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
23
|
+
end
|
24
|
+
|
25
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
28
|
+
spec.rcov = true
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task :default => :spec
|
33
|
+
|
34
|
+
require 'rake/rdoctask'
|
35
|
+
Rake::RDocTask.new do |rdoc|
|
36
|
+
if File.exist?('VERSION.yml')
|
37
|
+
config = YAML.load(File.read('VERSION.yml'))
|
38
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
39
|
+
else
|
40
|
+
version = ""
|
41
|
+
end
|
42
|
+
|
43
|
+
rdoc.rdoc_dir = 'rdoc'
|
44
|
+
rdoc.title = "proto_processor #{version}"
|
45
|
+
rdoc.rdoc_files.include('README*')
|
46
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
47
|
+
end
|
48
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'logger'
|
2
|
+
module ProtoProcessor
|
3
|
+
ROOT = File.dirname(__FILE__)
|
4
|
+
$: << File.join(ROOT, 'proto_processor')
|
5
|
+
|
6
|
+
autoload :Task, 'task'
|
7
|
+
autoload :Strategy, 'strategy'
|
8
|
+
autoload :TaskRunner, 'task_runner'
|
9
|
+
autoload :Report, 'report'
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :logger
|
13
|
+
end
|
14
|
+
end
|
15
|
+
ProtoProcessor.logger = Logger.new(STDOUT)
|
16
|
+
ProtoProcessor.logger.level = Logger::ERROR
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module ProtoProcessor
|
2
|
+
class Report
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :chain_outputs, :chain_tasks, :error, :chain_statuses
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@chain_outputs = {}
|
9
|
+
@chain_tasks = {}
|
10
|
+
|
11
|
+
@chain_statuses = []
|
12
|
+
@error = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# == Store tasks and output after running task chain
|
16
|
+
#
|
17
|
+
def report(chain_key, tasks, output)
|
18
|
+
report_output(chain_key, output)
|
19
|
+
report_tasks(chain_key, tasks)
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&block)
|
23
|
+
@chain_outputs.each &block
|
24
|
+
end
|
25
|
+
|
26
|
+
def [](chain_key)
|
27
|
+
@chain_outputs[chain_key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Major fail at strategy level
|
31
|
+
#
|
32
|
+
def fail!(exception)
|
33
|
+
@error = exception
|
34
|
+
end
|
35
|
+
|
36
|
+
def successful?
|
37
|
+
@error.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_report
|
41
|
+
@run_report ||= begin
|
42
|
+
rep = {}
|
43
|
+
@chain_tasks.each do |task_name, runs|
|
44
|
+
runs.each_with_index do |r, i|
|
45
|
+
rep[:"#{task_name}_#{i}"] = r.map{|t| "#{t.class.name}: #{t.report[:status]}"}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rep
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# {
|
55
|
+
# :FooTask => [out1,out2],
|
56
|
+
# :BarTask => [out1,out2]
|
57
|
+
# }
|
58
|
+
def report_output(chain_key, output)
|
59
|
+
@chain_outputs[chain_key] ||= []
|
60
|
+
@chain_outputs[chain_key] << output
|
61
|
+
end
|
62
|
+
|
63
|
+
# {
|
64
|
+
# :FooTask => [[task1,task2],[task1,task2]],
|
65
|
+
# :BarTask => [[task1,task2],[task1,task2]]
|
66
|
+
# }
|
67
|
+
|
68
|
+
|
69
|
+
def report_tasks(chain_key, tasks)
|
70
|
+
@chain_tasks[chain_key] ||= []
|
71
|
+
@chain_tasks[chain_key] << [*tasks]
|
72
|
+
|
73
|
+
@chain_statuses << tasks.map{|t|t.successful?}
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module ProtoProcessor::Strategy
|
2
|
+
|
3
|
+
def report
|
4
|
+
@report ||= ProtoProcessor::Report.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def runner
|
8
|
+
@task_runner ||= ProtoProcessor::TaskRunner.new(report)
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
begin
|
13
|
+
ProtoProcessor.logger.info "Running strategy #{self.class.name}"
|
14
|
+
process
|
15
|
+
rescue StandardError => e
|
16
|
+
report.fail!(e)
|
17
|
+
ProtoProcessor.logger.error e.class.name
|
18
|
+
ProtoProcessor.logger.debug e.message + "\n" + e.backtrace.join("\n")
|
19
|
+
end
|
20
|
+
yield report if block_given?
|
21
|
+
report
|
22
|
+
end
|
23
|
+
|
24
|
+
def process
|
25
|
+
raise NotImplementedError, "You must implement #process in your strategies"
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_input(input)
|
29
|
+
@current_input = input#.dup # dup so we don't overwrite passed input later on
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_input
|
33
|
+
@current_input ||= ''
|
34
|
+
end
|
35
|
+
|
36
|
+
# === Run a task and update input and report (but don't update options)
|
37
|
+
# If passed and array of options, run task for each option hash
|
38
|
+
#
|
39
|
+
def run_task(task_class, options = nil, &block)
|
40
|
+
return false if options.nil?
|
41
|
+
run_task_chain([*task_class], options, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def run_task_chain(task_classes, options, &block)
|
47
|
+
chain_key = task_classes.first.name.split('::').last.to_sym
|
48
|
+
@current_input, temp_options, task_report = runner.run_chain(chain_key, task_classes,current_input, options, {}, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
require 'set'
|
2
|
+
module ProtoProcessor
|
3
|
+
|
4
|
+
module Task
|
5
|
+
|
6
|
+
FAILURE = 'FAILURE'
|
7
|
+
SUCCESS = 'SUCCESS'
|
8
|
+
|
9
|
+
class InvalidTaskError < StandardError
|
10
|
+
def message
|
11
|
+
"Invalid task"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class MissingParametersError < InvalidTaskError
|
16
|
+
def initialize(errors)
|
17
|
+
@errors = errors
|
18
|
+
super
|
19
|
+
end
|
20
|
+
|
21
|
+
def message
|
22
|
+
"Missing parameters: #{@errors.inspect}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.included(base)
|
27
|
+
base.class_eval do
|
28
|
+
attr_reader :input, :options, :report, :error
|
29
|
+
extend Validations
|
30
|
+
end
|
31
|
+
#base.extend Validations
|
32
|
+
end
|
33
|
+
|
34
|
+
# new([input, options, report])
|
35
|
+
def initialize(args)
|
36
|
+
raise ArgumentError, "You must provide an Enumerable object as argument" unless args.respond_to?(:each)
|
37
|
+
raise ArgumentError, "You must provide an array with input, options and report" if args.size < 3
|
38
|
+
raise ArgumentError, "A task report must be or behave like a Hash" unless args.last.respond_to?(:[]=)
|
39
|
+
@input, @options, @report = args[0], args[1].dup, args[2].dup
|
40
|
+
@success = false
|
41
|
+
@error = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# class HaltedChainError < StandardError
|
45
|
+
# def message
|
46
|
+
# "Task not run because previous task failed"
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
|
50
|
+
def run
|
51
|
+
begin
|
52
|
+
log_halt_task and return false if report[:status] == FAILURE
|
53
|
+
run_validations!
|
54
|
+
before_process
|
55
|
+
process
|
56
|
+
report!(:status, SUCCESS)
|
57
|
+
@success = true
|
58
|
+
after_process
|
59
|
+
rescue StandardError => e
|
60
|
+
# horrible horrible hack to allow Rspec exceptions to bubble up.
|
61
|
+
# we can't rescue them because RSpec is only included when running specs
|
62
|
+
raise if e.class.name =~ /Spec/
|
63
|
+
report!(:status, FAILURE)
|
64
|
+
report!(:error, {:name => e.class.name, :message => e.message})
|
65
|
+
@error = e
|
66
|
+
ProtoProcessor.logger.error "#{self.class.name}: #{e.class.name} => #{e.message}"
|
67
|
+
ProtoProcessor.logger.debug e.backtrace.join("\n")
|
68
|
+
end
|
69
|
+
[@input, @options, @report]
|
70
|
+
end
|
71
|
+
|
72
|
+
def successful?
|
73
|
+
@success
|
74
|
+
end
|
75
|
+
|
76
|
+
# === Validate in subclasses
|
77
|
+
# Example:
|
78
|
+
# def validate
|
79
|
+
# options[:some].nil?
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
def valid?
|
83
|
+
run_validations!
|
84
|
+
true
|
85
|
+
rescue InvalidTaskError => e
|
86
|
+
false
|
87
|
+
end
|
88
|
+
|
89
|
+
# Abstract
|
90
|
+
#
|
91
|
+
def process
|
92
|
+
raise NotImplementedError, "You need to implement #process in you tasks"
|
93
|
+
end
|
94
|
+
|
95
|
+
def before_process
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def after_process
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
# Update input so it's passed to the next task
|
104
|
+
#
|
105
|
+
def update_input!(new_input)
|
106
|
+
@input = new_input
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def log_halt_task
|
112
|
+
ProtoProcessor.logger.info "#{self.class.name} not run because previous task failed"
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate
|
116
|
+
# implement this in subclasses if needed
|
117
|
+
true
|
118
|
+
end
|
119
|
+
|
120
|
+
def run_validations!
|
121
|
+
raise InvalidTaskError unless validate
|
122
|
+
errors = []
|
123
|
+
self.class.validations.each do |key, required|
|
124
|
+
provided = send(key).keys
|
125
|
+
missing = required - provided
|
126
|
+
errors << "#{key} => #{missing.inspect}" unless required.to_set.subset?(provided.to_set)
|
127
|
+
end
|
128
|
+
raise MissingParametersError.new(errors) unless errors.empty?
|
129
|
+
end
|
130
|
+
|
131
|
+
def report!(key, value)
|
132
|
+
@report[key] = value
|
133
|
+
end
|
134
|
+
|
135
|
+
module Validations
|
136
|
+
def expects_options_with(*args)
|
137
|
+
store_validations_for(:options, args)
|
138
|
+
end
|
139
|
+
|
140
|
+
def expects_report_with(*args)
|
141
|
+
store_validations_for(:report, args)
|
142
|
+
end
|
143
|
+
|
144
|
+
# {
|
145
|
+
# :options => [:field1, :field2],
|
146
|
+
# :report => [:field1, :field2]
|
147
|
+
# }
|
148
|
+
def store_validations_for(key, args)
|
149
|
+
validations[key] ||= []
|
150
|
+
validations[key] += args
|
151
|
+
end
|
152
|
+
|
153
|
+
def validations
|
154
|
+
@validations ||= {}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ProtoProcessor
|
2
|
+
class TaskRunner
|
3
|
+
|
4
|
+
attr_reader :report
|
5
|
+
|
6
|
+
def initialize(report)
|
7
|
+
@report = report
|
8
|
+
end
|
9
|
+
|
10
|
+
def run_chain(run_key, task_classes, initial_input, options, initial_report, &block)
|
11
|
+
tasks = []
|
12
|
+
output = task_classes.inject([initial_input, options, initial_report]) do |args, task_class|
|
13
|
+
task = task_class.new(args)
|
14
|
+
tasks << task
|
15
|
+
task.run
|
16
|
+
end
|
17
|
+
block.call(tasks, output.first, output.last) if block_given?
|
18
|
+
@report.report(run_key, tasks, output)
|
19
|
+
output
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
module BAS
|
4
|
+
class BAS::FooTask
|
5
|
+
include ProtoProcessor::Task
|
6
|
+
def process
|
7
|
+
@input << 'FOO'
|
8
|
+
report!(:hello, @input)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class BAS::BarTask
|
13
|
+
include ProtoProcessor::Task
|
14
|
+
def process
|
15
|
+
@input << 'BAR'
|
16
|
+
report!(:hello, @input)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class FooBarStrategy
|
22
|
+
include ProtoProcessor::Strategy
|
23
|
+
|
24
|
+
def options
|
25
|
+
options = {
|
26
|
+
"original" => 'sample_image.jpg',
|
27
|
+
"type" => "FooBar",
|
28
|
+
"rotate" => 90,
|
29
|
+
"crop" => 'crop_options',
|
30
|
+
"sizes" => [{'width' => 100}, {'width' => 200}, {'width' => 300}]
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def process
|
35
|
+
run_task BAS::FooTask, options
|
36
|
+
run_task BAS::BarTask, options
|
37
|
+
options['sizes'].each do |size_params|
|
38
|
+
run_task BAS::FooTask, size_params
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "Strategy" do
|
46
|
+
before do
|
47
|
+
@strategy = FooBarStrategy.new
|
48
|
+
@options = @strategy.options
|
49
|
+
end
|
50
|
+
describe 'running tasks with #run_task' do
|
51
|
+
before do
|
52
|
+
@mock_task = mock('task', :run => true, :valid? => true, :successful? => true)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should have a task runner" do
|
56
|
+
@strategy.runner.should be_kind_of(ProtoProcessor::TaskRunner)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should have an initial report" do
|
60
|
+
@strategy.report.should be_kind_of(ProtoProcessor::Report)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should delegate to Task Runner with run key, array of one task, input, options and report" do
|
64
|
+
@strategy.runner.should_receive(:run_chain).with(:FooTask, [BAS::FooTask], @strategy.current_input, @options, {})
|
65
|
+
@strategy.run_task BAS::FooTask, @options
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be able to change the input for upcoming tasks" do
|
69
|
+
new_input = 'a different input'
|
70
|
+
@strategy.runner.should_receive(:run_chain).with(:FooTask, [BAS::FooTask], new_input, @options, {})
|
71
|
+
@strategy.with_input new_input
|
72
|
+
@strategy.run_task BAS::FooTask, @options
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should run a task with default input, options and report" do
|
76
|
+
BAS::FooTask.should_receive(:new).with([@strategy.current_input,@options,{}]).and_return @mock_task
|
77
|
+
@strategy.run_task BAS::FooTask, @options
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should not run a task if passed options are NIL" do
|
81
|
+
BAS::FooTask.should_not_receive(:new)
|
82
|
+
@strategy.run_task BAS::FooTask
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should run a task with passed options" do
|
86
|
+
opts = {:blah => 1}
|
87
|
+
BAS::FooTask.should_receive(:new).with([@strategy.current_input,opts,{}]).and_return @mock_task
|
88
|
+
@strategy.run_task BAS::FooTask, opts
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'running main process' do
|
94
|
+
before do
|
95
|
+
@mock_task = mock('task', :run => [@input, @options, {}], :valid? => true)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should accumulate result of tasks" do
|
99
|
+
pending 'this belongs in the task runner'
|
100
|
+
BAS::FooTask.should_receive(:new).exactly(4).and_return @mock_task
|
101
|
+
BAS::BarTask.should_receive(:new).exactly(1).and_return @mock_task
|
102
|
+
@strategy.run
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should update values" do
|
106
|
+
report = @strategy.run
|
107
|
+
@strategy.current_input.should == 'FOOBARFOOFOOFOO'
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
class FooTask
|
4
|
+
include ProtoProcessor::Task
|
5
|
+
def process
|
6
|
+
@input << 'a'
|
7
|
+
report! :foo, 'foo'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InvalidTask
|
12
|
+
include ProtoProcessor::Task
|
13
|
+
def process
|
14
|
+
@input = "I got here"
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "Task" do
|
23
|
+
before do
|
24
|
+
@input = ''
|
25
|
+
@options = {}
|
26
|
+
@report = {}
|
27
|
+
@task = FooTask.new([@input, @options, @report])
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should raise if less than 3 arguments" do
|
31
|
+
lambda {
|
32
|
+
FooTask.new(['bar'])
|
33
|
+
}.should raise_error(ArgumentError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should raise if argument is not enumerable" do
|
37
|
+
lambda {
|
38
|
+
FooTask.new(1)
|
39
|
+
}.should raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should raise if report does not cuack like a Hash" do
|
43
|
+
lambda {
|
44
|
+
FooTask.new(['',{},10])
|
45
|
+
}.should raise_error(ArgumentError)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be valid by default" do
|
49
|
+
@task.valid?.should be_true
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should have input, options and report" do
|
53
|
+
@task.input.should == @input
|
54
|
+
@task.options.should == @options
|
55
|
+
@task.report.should == @report
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not be successful before running" do
|
59
|
+
@task.successful?.should_not be_true
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should update input with #update_input!" do
|
63
|
+
@task.input.should == ''
|
64
|
+
@task.update_input!(2)
|
65
|
+
@task.input.should == 2
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "invalid tasks" do
|
69
|
+
before do
|
70
|
+
@invalid_task = InvalidTask.new(['', {}, @report])
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should be invalid (duh!)" do
|
74
|
+
@invalid_task.valid?.should be_false
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should not process if not valid" do
|
78
|
+
# if the input is modified, it means that it did run #process
|
79
|
+
# we can't use expectations because I'm rescueing exceptions, including RSpec ones!
|
80
|
+
output = @invalid_task.run
|
81
|
+
output.first.should be_empty
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'task receiving failed report' do
|
87
|
+
before do
|
88
|
+
@task = FooTask.new([@input, @options, {:status => 'FAILURE'}])
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should halt processing" do
|
92
|
+
@task.should_not_receive(:process)
|
93
|
+
@task.run
|
94
|
+
@task.should_not be_successful
|
95
|
+
@task.report[:status].should == 'FAILURE'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "running" do
|
100
|
+
|
101
|
+
it "should invoke :process" do
|
102
|
+
@task.should_receive(:process)
|
103
|
+
@task.run
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should return modified input" do
|
107
|
+
output = @task.run
|
108
|
+
output[0].should == 'a'
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should return options" do
|
112
|
+
output = @task.run
|
113
|
+
output[1].should == @options
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should add stuff to report" do
|
117
|
+
output = @task.run
|
118
|
+
output[2].should == {:status => 'SUCCESS', :foo => 'foo'}
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should be successful after running (successfully)" do
|
122
|
+
@task.run
|
123
|
+
@task.successful?.should be_true
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
describe 'process callbacks' do
|
129
|
+
before :all do
|
130
|
+
class ATask
|
131
|
+
include ProtoProcessor::Task
|
132
|
+
def before_process
|
133
|
+
@input.before
|
134
|
+
end
|
135
|
+
def process
|
136
|
+
@input.process
|
137
|
+
end
|
138
|
+
def after_process
|
139
|
+
@input.after
|
140
|
+
end
|
141
|
+
end
|
142
|
+
@input = mock('input', :before =>1, :process => 2, :after => 3)
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should invoke after and before process callbacks" do
|
147
|
+
@input.should_receive(:before)
|
148
|
+
@input.should_receive(:process)
|
149
|
+
@input.should_receive(:after)
|
150
|
+
ATask.new([@input, {}, {}]).run
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
class BarTask
|
157
|
+
include ProtoProcessor::Task
|
158
|
+
def process
|
159
|
+
@input << 'b'
|
160
|
+
report! :bar, 'bar'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "decorating the same or other tasks" do
|
165
|
+
|
166
|
+
it "should iteratively process" do
|
167
|
+
input, options, report = '', {}, {}
|
168
|
+
1.upto(5) do |i|
|
169
|
+
task = FooTask.new([input, options, report])
|
170
|
+
old_input = input
|
171
|
+
input, options, report = task.run
|
172
|
+
old_input.object_id.should == input.object_id
|
173
|
+
end
|
174
|
+
|
175
|
+
task = BarTask.new([input, options, report])
|
176
|
+
input, options, report = task.run
|
177
|
+
|
178
|
+
input.should == 'aaaaab'
|
179
|
+
report.should == {:bar=>"bar", :status=>"SUCCESS", :foo=>"foo"}
|
180
|
+
end
|
181
|
+
end
|
data/spec/report_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
module TVS
|
4
|
+
class MockTask
|
5
|
+
include ProtoProcessor::Task
|
6
|
+
end
|
7
|
+
class FooTask < MockTask
|
8
|
+
|
9
|
+
expects_options_with :id, :name
|
10
|
+
|
11
|
+
expects_report_with :url, :path
|
12
|
+
|
13
|
+
def process
|
14
|
+
puts options[:id]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'Task validations' do
|
21
|
+
describe "with valid parameters" do
|
22
|
+
before do
|
23
|
+
@task = TVS::FooTask.new(['',{:id => 1, :name => 'bar'},{:url => 'abc', :path => 'aaa'}])
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be valid" do
|
27
|
+
@task.should be_valid
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "with missing parameters" do
|
32
|
+
before do
|
33
|
+
@task = TVS::FooTask.new(['',{:id => 1},{:path => 'aaa'}])
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should not be valid" do
|
37
|
+
@task.should_not be_valid
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not run" do
|
41
|
+
@task.should_not_receive(:process)
|
42
|
+
@task.run
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should pass status report = FAILURE" do
|
46
|
+
@task.run
|
47
|
+
@task.report[:status].should == ProtoProcessor::Task::FAILURE
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should have error information" do
|
51
|
+
@task.run
|
52
|
+
@task.report[:error].should_not be_nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
module RunnerSpecHelper
|
4
|
+
class Task1
|
5
|
+
include ProtoProcessor::Task
|
6
|
+
def process
|
7
|
+
report! :common, 1
|
8
|
+
report! :a, 1
|
9
|
+
@input << 'a'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
class Task2
|
13
|
+
include ProtoProcessor::Task
|
14
|
+
def process
|
15
|
+
report! :common, 2
|
16
|
+
report! :b, 2
|
17
|
+
@input << 'b'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
class Task3
|
21
|
+
include ProtoProcessor::Task
|
22
|
+
def process
|
23
|
+
report! :common, 3
|
24
|
+
report! :c, 3
|
25
|
+
@input << 'c'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
include RunnerSpecHelper
|
30
|
+
include ProtoProcessor
|
31
|
+
|
32
|
+
describe '@runner' do
|
33
|
+
before do
|
34
|
+
@input, @options, @task_report = '', {}, {}
|
35
|
+
@report = mock('report', :report => true)
|
36
|
+
@runner = TaskRunner.new(@report)
|
37
|
+
end
|
38
|
+
describe 'running tasks with #run_chain' do
|
39
|
+
|
40
|
+
before do
|
41
|
+
@task1 = mock('task1', :run => [@input, @options, {:common => 1}])
|
42
|
+
@task2 = mock('task2', :run => [@input, @options, {:common => 2}])
|
43
|
+
@task3 = mock('task3', :run => [@input, @options, {:common => 3}])
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should run one task" do
|
47
|
+
Task1.should_receive(:new).with([@input, @options, @task_report]).and_return @task1
|
48
|
+
@runner.run_chain(:foo,[Task1], @input, @options, @task_report).should == ["", {}, {:common=>1}]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should register task and output with report object" do
|
52
|
+
Task1.stub!(:new).and_return @task1
|
53
|
+
@report.should_receive(:report).with(:foo, [@task1], ["", {}, {:common=>1}])
|
54
|
+
@runner.run_chain(:foo,[Task1], @input, @options, @task_report)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should run sequence of nested tasks" do
|
58
|
+
Task1.should_receive(:new).with([@input, @options, @task_report]).and_return @task1
|
59
|
+
Task2.should_receive(:new).with([@input, @options, @task_report.merge(:common => 1)]).and_return @task2
|
60
|
+
Task3.should_receive(:new).with([@input, @options, @task_report.merge(:common => 2)]).and_return @task3
|
61
|
+
|
62
|
+
@runner.run_chain(:foo,[Task1, Task2, Task3], @input, @options, @task_report)
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should call an optional block with run tasks, final output and consolidated report" do
|
69
|
+
collaborator = mock('collaborator')
|
70
|
+
collaborator.should_receive(:do_something!).with(2, "ab", {:b=>2, :status=>"SUCCESS", :common=>2, :a=>1})
|
71
|
+
@runner.run_chain(:foo,[Task1, Task2], @input, @options, @task_report) do |tasks, output, report|
|
72
|
+
collaborator.do_something!(tasks.size, output, report)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "verifies that result is merged hash" do
|
77
|
+
@runner.run_chain(:foo,[Task1, Task2, Task3], @input, @options, @task_report)\
|
78
|
+
.should == ['abc', @options, {:common => 3, :status => 'SUCCESS', :a =>1, :b =>2, :c => 3}]
|
79
|
+
end
|
80
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: newbamboo-proto_processor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ismael Celis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-05-26 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: ismaelct@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.markdown
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.markdown
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- lib/proto_processor.rb
|
31
|
+
- lib/proto_processor/report.rb
|
32
|
+
- lib/proto_processor/strategy.rb
|
33
|
+
- lib/proto_processor/task.rb
|
34
|
+
- lib/proto_processor/task_runner.rb
|
35
|
+
- spec/base_strategy_spec.rb
|
36
|
+
- spec/base_task_spec.rb
|
37
|
+
- spec/report_spec.rb
|
38
|
+
- spec/spec_helper.rb
|
39
|
+
- spec/task_validations_spec.rb
|
40
|
+
- spec/tasks_runner_spec.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/ismasan/proto_processor
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --charset=UTF-8
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.2.0
|
64
|
+
signing_key:
|
65
|
+
specification_version: 2
|
66
|
+
summary: TODO
|
67
|
+
test_files:
|
68
|
+
- spec/base_strategy_spec.rb
|
69
|
+
- spec/base_task_spec.rb
|
70
|
+
- spec/report_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/task_validations_spec.rb
|
73
|
+
- spec/tasks_runner_spec.rb
|