dynflow 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'multi_json'
4
+ gem 'activesupport'
5
+ gem 'apipie-params', :git => '/home/inecas/Projects/apipie-params'
6
+
7
+ group :development do
8
+ gem 'pry'
9
+ end
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Pavel Pokorný
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.md ADDED
@@ -0,0 +1,151 @@
1
+ DYNamic workFLOW
2
+ ================
3
+
4
+ In traditional workflow engines, you specify a static workflow and
5
+ then run it with various inputs. Dynflow takes different approach.
6
+
7
+ You specify the inputs and the workflow is generated on the fly. You
8
+ can either specify the steps explicitly or subscribe one action to
9
+ another. This is suitable for plugin architecture, where you can't
10
+ write the whole process on one place.
11
+
12
+ Dynflow doesn't differentiate between workflow and action. Instead,
13
+ every action can populate another actions, effectively producing the
14
+ resulting set of steps.
15
+
16
+ The whole execution is done in three phases:
17
+
18
+ 1. *Planning phase*
19
+
20
+ Construct the execution plan for the workflow. It's invoked by
21
+ calling `trigger` on an action. Two mechanisms are used to get the set
22
+ of actions to be executed:
23
+
24
+ a. explicit calls of `plan_action` methods in the `plan` method
25
+
26
+ b. implicit associations: an action A subscribes to an action B,
27
+ which means that the action A is executed whenever the action B
28
+ occurs.
29
+
30
+ The output of this phase is a set of actions and their inputs.
31
+
32
+ 2. *Execution phase*
33
+
34
+ The plan is being executed step by step, calling the run method of
35
+ an action with corresponding input. The results of every action are
36
+ written into output attribute.
37
+
38
+ The run method should be stateless, with all the needed information
39
+ included in the input from planning phase. This allows us to
40
+ control the workflow execution: the state of every action can be
41
+ serialized therefore the workflow itself can be persisted. This makes
42
+ it easy to recover from failed actions by rerunning it.
43
+
44
+ 3. *Finalization phase*
45
+
46
+ Take the results from the execution phase and perform some additional
47
+ tasks. This is suitable for example for recording the results into
48
+ database.
49
+
50
+ Every action can participate in every phase.
51
+
52
+ Example
53
+ -------
54
+
55
+ One code snippet is worth 1000 words:
56
+
57
+ ```ruby
58
+ # The anatomy of action class
59
+
60
+ # every action needs to inherit from Dynflow::Action
61
+ class Action < Dynflow::Action
62
+
63
+ # OPTIONAL: the input format for the execution phase of this action
64
+ # (https://github.com/iNecas/apipie-params for more details.
65
+ # Validations can be performed against this description (turned off
66
+ # for now)
67
+ input_format do
68
+ param :id, Integer
69
+ param :name, String
70
+ end
71
+
72
+ # OPTIONAL: every action can produce an output in the execution
73
+ # phase. This allows to describe the output.
74
+ output_format do
75
+ param :uuid, String
76
+ end
77
+
78
+ # OPTIONAL: this specifies that this action should be performed when
79
+ # AnotherAction is triggered.
80
+ def self.subscribe
81
+ AnotherAction
82
+ end
83
+
84
+ # OPTIONAL: executed during the planning phase. It's possible to
85
+ # specify explicitly the workflow here. By default it schedules just
86
+ # this action.
87
+ def plan(object_1, object_2)
88
+ # +plan_action+ schedules the SubAction to be part of this
89
+ # workflow
90
+ # the +object_1+ is passed to the +SubAction#plan+ method.
91
+ plan_action SubAction, object_1
92
+ # we can specify, where in the workflow this action should be
93
+ # placed, as well as prepare the input.
94
+ plan_self {'id' => object_2.id, 'name' => object_2.name}
95
+ end
96
+
97
+ # OPTIONAL: run the execution part of this action. Transform the
98
+ # data from +input+ to +output+. When not specified, the action is
99
+ # not used in the execution phase.
100
+ def run
101
+ output['uuid'] = "#{input['name']}-#{input['id']}"
102
+ end
103
+
104
+ # OPTIONAL: finalize the action after the execution phase finishes.
105
+ # in the +input+ and +output+ attributes are available the data from
106
+ # execution phase. in the +outputs+ argument, all the execution
107
+ # phase actions are available, each providing its input and output.
108
+ def finalize(outputs)
109
+ puts output['uuid']
110
+ end
111
+ end
112
+ ```
113
+
114
+ One can generate the execution plan for an action without actually
115
+ running it:
116
+
117
+ ```ruby
118
+ pp Publish.plan(short_article)
119
+ # the expanded workflow is:
120
+ # [
121
+ # [Publish, {"title"=>"Short", "body"=>"Short"}],
122
+ # [Review, {"title"=>"Short", "body"=>"Short"}],
123
+ # [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
124
+ # ]
125
+ ```
126
+
127
+ Therefore it's suitable for the plan methods to not have any side
128
+ effects (except of database writes that can be roll-backed)
129
+
130
+ In the finalization phase, `finalize` method is called on every action
131
+ if defined. The order is the same as in the execution plan.
132
+
133
+ Every action should be as atomic as possible, providing better
134
+ granularity when manipulating the process. Since every action can be
135
+ subscribed by another one, adding new behaviour to an existing
136
+ workflow is really simple.
137
+
138
+ The input and output format can be used for defining the interface
139
+ that other developers can use when extending the workflows.
140
+
141
+ See the examples directory for more complete examples.
142
+
143
+ License
144
+ -------
145
+
146
+ MIT
147
+
148
+ Author
149
+ ------
150
+
151
+ Ivan Nečas
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << 'lib' << 'test'
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ t.verbose = true
7
+ end
data/dynflow.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "dynflow/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "dynflow"
7
+ s.version = Dynflow::VERSION
8
+ s.authors = ["Ivan Necas"]
9
+ s.email = ["inecas@redhat.com"]
10
+ s.homepage = "http://github.com/iNecas/eventum"
11
+ s.summary = "DYNamic workFLOW engine"
12
+ s.description = "Generate and executed workflows dynamically based "+
13
+ "on input data and leave it open for others to jump into it as well"
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency "activesupport"
20
+ s.add_dependency "multi_json"
21
+ s.add_dependency "apipie-params"
22
+ s.add_development_dependency "minitest"
23
+ end
@@ -0,0 +1,71 @@
1
+ # Shows how Dynflow can be used for events architecture: actions are
2
+ # subscribed to an event. When the event is triggered all the
3
+ # subscribed actions are preformed.
4
+
5
+ $:.unshift(File.expand_path('../../lib', __FILE__))
6
+
7
+ require 'dynflow'
8
+ require 'pp'
9
+
10
+ # this is an event that can be triggered.
11
+ # it has an input format so that the interface is given
12
+ # TODO: the validations are turned off right now
13
+ class Click < Dynflow::Action
14
+ input_format do
15
+ param :x, Integer
16
+ param :y, Integer
17
+ end
18
+ end
19
+
20
+ # SayHello subscibes to the event: it's run when the event is triggered
21
+ class SayHello < Dynflow::Action
22
+
23
+ def self.subscribe
24
+ Click
25
+ end
26
+
27
+ def run
28
+ puts "Hello World"
29
+ end
30
+ end
31
+
32
+ # we can subscribe more actions to an event
33
+ class SayPosition < Dynflow::Action
34
+
35
+ def self.subscribe
36
+ Click
37
+ end
38
+
39
+ def run
40
+ puts "your position is [#{input['x']} - #{input['y']}]"
41
+ end
42
+
43
+ end
44
+
45
+ # we can even subscribe to an action that is subscribed to an event
46
+ class SayGoodbye < Dynflow::Action
47
+
48
+ def self.subscribe
49
+ SayPosition
50
+ end
51
+
52
+ def run
53
+ puts "Good Bye"
54
+ end
55
+ end
56
+
57
+ Click.trigger('x' => 5, 'y' => 4)
58
+ # gives us:
59
+ # Hello World
60
+ # your position is [5 - 4]
61
+ # Good Bye
62
+
63
+ pp Click.plan('x' => 5, 'y' => 4)
64
+ # returns the execution plan for the event (nothing is triggered):
65
+ # [
66
+ # since the event is action as well, it could have a run method
67
+ # [Click, {"x"=>5, "y"=>4}],
68
+ # [SayHello, {"x"=>5, "y"=>4}],
69
+ # [SayPosition, {"x"=>5, "y"=>4}],
70
+ # [SayGoodbye, {"x"=>5, "y"=>4}]
71
+ # ]
@@ -0,0 +1,140 @@
1
+ # Shows how Dynflow can be used for dynamic workflow definition
2
+ # and execution.
3
+ # In a planning phase of an action, a sub-action can be planned as
4
+ # well.
5
+
6
+
7
+ $:.unshift(File.expand_path('../../lib', __FILE__))
8
+
9
+ require 'dynflow'
10
+ require 'pp'
11
+
12
+ class Article < Struct.new(:title, :body, :color); end
13
+
14
+ class Publish < Dynflow::Action
15
+ input_format do
16
+ param :title, Integer
17
+ param :body, Integer
18
+ end
19
+
20
+ # plan can take arbitrary arguments. The args are passed from the
21
+ # trigger method.
22
+ def plan(article)
23
+ # we can explicitly plan a subaction
24
+ plan_self 'title' => article.title, 'body' => article.body
25
+ plan_action Review, article
26
+ end
27
+
28
+ def run
29
+ puts 'Starting'
30
+ end
31
+
32
+ # after all actions are run, there is a finishing phase. All the
33
+ # actions with +finished+ action defined are called, passing all the
34
+ # performed actions (with inputs and outputs)
35
+ def finalize(outputs)
36
+ printer_action = outputs.find { |o| o.is_a? Print }
37
+ puts "Printer says '#{printer_action.output['message']}'"
38
+ end
39
+ end
40
+
41
+ class Review < Dynflow::Action
42
+
43
+ # the actions can provide an output for the finalizing phase
44
+ output_format do
45
+ param :rating, Integer
46
+ end
47
+
48
+ # the plan method takes the same arguments as the parent action
49
+ def plan(article)
50
+ # in the input attribute the input for the parent action is
51
+ # available
52
+ plan_self input
53
+ end
54
+
55
+ # if no plan method given, the input is the same as the action that
56
+ # triggered it
57
+ def run
58
+ puts "Reviewing #{input['title']}"
59
+ raise "Too Short" if input['body'].size < 6
60
+ output['rating'] = input['body'].size
61
+ end
62
+
63
+ def finalize(outputs)
64
+ # +input+ and +output+ attributes are available in the finalizing
65
+ # phase as well.
66
+ puts "The rating was #{output['rating']}"
67
+ end
68
+
69
+ end
70
+
71
+ class Print < Dynflow::Action
72
+
73
+ input_format do
74
+ param :title, Integer
75
+ param :body, Integer
76
+ param :color, :boolean
77
+ end
78
+
79
+ output_format do
80
+ param :message, String
81
+ end
82
+
83
+ # if needed, we can subscribe to an action instead of explicitly
84
+ # specifying it in the plan method. Suitable for plugin architecture.
85
+ def self.subscribe
86
+ Review # sucessful review means we can print
87
+ end
88
+
89
+ def plan(article)
90
+ plan_self input.merge('color' => article.color)
91
+ end
92
+
93
+ def run
94
+ if input['color']
95
+ puts "Printing in color"
96
+ else
97
+ puts "Printing blank&white"
98
+ end
99
+ output['message'] = "Here you are"
100
+ end
101
+ end
102
+
103
+ short_article = Article.new('Short', 'Short', false)
104
+ long_article = Article.new('Long', 'This is long', false)
105
+ colorful_article = Article.new('Long Color', 'This is long in color', true)
106
+
107
+ pp Publish.plan(short_article)
108
+ # the expanded workflow is:
109
+ # [
110
+ # [Publish, {"title"=>"Short", "body"=>"Short"}],
111
+ # [Review, {"title"=>"Short", "body"=>"Short"}],
112
+ # [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
113
+ # ]
114
+
115
+ begin
116
+ Publish.trigger(short_article)
117
+ rescue => e
118
+ puts e.message
119
+ end
120
+ # Produces:
121
+ # Starting
122
+ # Reviewing Short
123
+ # Too Short
124
+
125
+ Publish.trigger(long_article)
126
+ # Produces:
127
+ # Starting
128
+ # Reviewing Long
129
+ # Printing blank&white
130
+ # Printer says 'Here you are'
131
+ # The rating was 12
132
+
133
+
134
+ Publish.trigger(colorful_article)
135
+ # Produces:
136
+ # Starting
137
+ # Reviewing Long Color
138
+ # Printing in color
139
+ # Printer says 'Here you are'
140
+ # The rating was 21
data/lib/dynflow.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'dynflow/logger'
2
+ require 'dynflow/message'
3
+ require 'dynflow/dispatcher'
4
+ require 'dynflow/bus'
5
+ require 'dynflow/orch_request'
6
+ require 'dynflow/orch_response'
7
+ require 'dynflow/action'
8
+
9
+ module Dynflow
10
+
11
+ end
@@ -0,0 +1,121 @@
1
+ module Dynflow
2
+ class Action < Message
3
+
4
+ # only for the planning phase: flag indicating that the action
5
+ # was triggered from subscription. If so, the implicit plan
6
+ # method uses the input of the parent action. Otherwise, the
7
+ # argument the plan_action is used as default.
8
+ attr_accessor :from_subscription
9
+
10
+ def self.inherited(child)
11
+ self.actions << child
12
+ end
13
+
14
+ def self.actions
15
+ @actions ||= []
16
+ end
17
+
18
+ def self.subscribe
19
+ nil
20
+ end
21
+
22
+ def self.require
23
+ nil
24
+ end
25
+
26
+ def initialize(input, output = {})
27
+ # for preparation phase
28
+ @execution_plan = []
29
+
30
+ output ||= {}
31
+ super('input' => input, 'output' => output)
32
+ end
33
+
34
+ def input
35
+ @data['input']
36
+ end
37
+
38
+ def input=(input)
39
+ @data['input'] = input
40
+ end
41
+
42
+ def output
43
+ @data['output']
44
+ end
45
+
46
+ # the block contains the expression in Apipie::Params::DSL
47
+ # describing the format of message
48
+ def self.input_format(&block)
49
+ if block
50
+ @input_format_block = block
51
+ elsif @input_format_block
52
+ @input_format ||= Apipie::Params::Description.define(&@input_format_block)
53
+ else
54
+ nil
55
+ end
56
+ end
57
+
58
+ # the block contains the expression in Apipie::Params::DSL
59
+ # describing the format of message
60
+ def self.output_format(&block)
61
+ if block
62
+ @output_format_block = block
63
+ elsif @output_format_block
64
+ @output_format ||= Apipie::Params::Description.define(&@output_format_block)
65
+ else
66
+ nil
67
+ end
68
+ end
69
+
70
+ def self.trigger(*args)
71
+ Dynflow::Bus.trigger(self.plan(*args))
72
+ end
73
+
74
+ def self.plan(*args)
75
+ action = self.new({})
76
+ yield action if block_given?
77
+ action.plan(*args)
78
+ action.add_subscriptions(*args)
79
+ action.execution_plan
80
+ end
81
+
82
+ # for subscribed actions: by default take the input of the
83
+ # subscribed action
84
+ def plan(*args)
85
+ if from_subscription
86
+ # if the action is triggered by subscription, by default use the
87
+ # input of parent action
88
+ plan_self(self.input)
89
+ else
90
+ # in this case, the action was triggered by plan_action. Use
91
+ # the argument specified there.
92
+ plan_self(args.first)
93
+ end
94
+ end
95
+
96
+ def plan_self(input)
97
+ self.input = input
98
+ @execution_plan << [self.class, input]
99
+ end
100
+
101
+ def plan_action(action_class, *args)
102
+ sub_action_plan = action_class.plan(*args) do |action|
103
+ action.input = self.input
104
+ end
105
+ @execution_plan.concat(sub_action_plan)
106
+ end
107
+
108
+ def add_subscriptions(*plan_args)
109
+ @execution_plan.concat(Dispatcher.execution_plan_for(self, *plan_args))
110
+ end
111
+
112
+ def execution_plan
113
+ @execution_plan
114
+ end
115
+
116
+ def validate!
117
+ self.clss.output_format.validate!(@data['output'])
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_support/inflector'
2
+ require 'forwardable'
3
+ module Dynflow
4
+ class Bus
5
+
6
+ class << self
7
+ extend Forwardable
8
+
9
+ def_delegators :impl, :wait_for, :process, :trigger, :finalize
10
+
11
+ def impl
12
+ @impl ||= Bus::MemoryBus.new
13
+ end
14
+ attr_writer :impl
15
+ end
16
+
17
+ def finalize(outputs)
18
+ outputs.each do |action|
19
+ if action.respond_to?(:finalize)
20
+ action.finalize(outputs)
21
+ end
22
+ end
23
+ end
24
+
25
+ def process(action_class, input, output = nil)
26
+ # TODO: here goes the message validation
27
+ action = action_class.new(input, output)
28
+ action.run if action.respond_to?(:run)
29
+ return action
30
+ end
31
+
32
+ def wait_for(*args)
33
+ raise NotImplementedError, 'Abstract method'
34
+ end
35
+
36
+ def logger
37
+ @logger ||= Dynflow::Logger.new(self.class)
38
+ end
39
+
40
+ class MemoryBus < Bus
41
+
42
+ def initialize
43
+ super
44
+ end
45
+
46
+ def trigger(execution_plan)
47
+ outputs = []
48
+ execution_plan.each do |(action_class, input)|
49
+ outputs << self.process(action_class, input)
50
+ end
51
+ self.finalize(outputs)
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ module Dynflow
2
+ class Dispatcher
3
+ class << self
4
+ def finalizers
5
+ @finalizers ||= Hash.new { |h, k| h[k] = [] }
6
+ end
7
+
8
+ def subscribed_actions(action)
9
+ Action.actions.find_all do |sub_action|
10
+ case sub_action.subscribe
11
+ when Hash
12
+ sub_action.subscribe.keys.include?(action.class)
13
+ when Array
14
+ sub_action.subscribe.include?(action.class)
15
+ else
16
+ sub_action.subscribe == action.class
17
+ end
18
+ end
19
+ end
20
+
21
+ def execution_plan_for(action, *plan_args)
22
+ ordered_actions = subscribed_actions(action).sort_by(&:name)
23
+
24
+ execution_plan = []
25
+ ordered_actions.each do |action_class|
26
+ sub_action_plan = action_class.plan(*plan_args) do |sub_action|
27
+ sub_action.input = action.input
28
+ sub_action.from_subscription = true
29
+ end
30
+ execution_plan.concat(sub_action_plan)
31
+ end
32
+ return execution_plan
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+
4
+ module Dynflow
5
+ class Logger
6
+ extend Forwardable
7
+
8
+ def_delegators :@impl, :debug, :info, :warn; :error
9
+
10
+ class DummyLogger < ::Logger
11
+ def initialize(identifier)
12
+ super(nil)
13
+ end
14
+ end
15
+
16
+
17
+ def initialize(identifier, impl = nil)
18
+ @impl = self.class.logger_class.new(identifier)
19
+ end
20
+
21
+ class << self
22
+
23
+ def logger_class
24
+ unless @logger_class
25
+ @logger_class ||= DummyLogger
26
+ end
27
+ return @logger_class
28
+ end
29
+
30
+ attr_writer :logger_class
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'apipie-params'
5
+
6
+ module Dynflow
7
+ class Message
8
+
9
+ def ==(other)
10
+ self.encode == other.encode
11
+ end
12
+
13
+ extend Forwardable
14
+
15
+ def_delegators :@data, '[]', '[]='
16
+
17
+ attr_reader :data
18
+
19
+ def initialize(data = {})
20
+ @data = data.with_indifferent_access
21
+ end
22
+
23
+
24
+ def self.decode(data)
25
+ ret = data['message_type'].constantize.allocate
26
+ ret.instance_variable_set("@data", data['data'])
27
+ return ret
28
+ end
29
+
30
+ def encode
31
+ {
32
+ 'message_type' => self.class.name,
33
+ 'data' => @data
34
+ }
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ module Dynflow
2
+ class OrchRequest < Message
3
+ def self.response_class
4
+ unless self.name =~ /::Request\Z/
5
+ raise "Unexpected class name, #{self.name} expected to end with ::Request"
6
+ end
7
+ begin
8
+ self.name.sub(/::Request\Z/, '::Response').constantize
9
+ rescue NameError => e
10
+ OrchResponse
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Dynflow
2
+ class OrchResponse < Message
3
+
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Dynflow
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+
3
+ module Dynflow
4
+ class CloneRepo < Action
5
+
6
+ output_format do
7
+ param :id, String
8
+ end
9
+
10
+ def run
11
+ output['id'] = input['name']
12
+ end
13
+
14
+ end
15
+
16
+ class CloneRepoTest < ParticipantTestCase
17
+
18
+ def test_action
19
+ action = run_action(CloneRepo, {:name => "zoo"})
20
+ assert_equal(action.output['id'], "zoo")
21
+ end
22
+
23
+ end
24
+ end
data/test/bus_test.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'test_helper'
2
+ require 'set'
3
+
4
+ module Dynflow
5
+ class BusTest < BusTestCase
6
+ class Promotion < Action
7
+
8
+ def plan(repo_names, package_names)
9
+ repo_names.each do |repo_name|
10
+ plan_action(CloneRepo, {'name' => repo_name})
11
+ end
12
+
13
+ package_names.each do |package_name|
14
+ plan_action(ClonePackage, {'name' => package_name})
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ class CloneRepo < Action
21
+
22
+ input_format do
23
+ param :name, String
24
+ end
25
+
26
+ output_format do
27
+ param :id, String
28
+ end
29
+
30
+ def run
31
+ output['id'] = input['name']
32
+ end
33
+
34
+ end
35
+
36
+ def execution_plan
37
+ [
38
+ [CloneRepo, {'name' => 'zoo'}],
39
+ [CloneRepo, {'name' => 'foo'}],
40
+ ]
41
+ end
42
+
43
+ def test_optimistic_case
44
+ expect_input(CloneRepo, {'name' => 'zoo'}, {'id' => '123'})
45
+ expect_input(CloneRepo, {'name' => 'foo'}, {'id' => '456'})
46
+ first_action, second_action = assert_scenario
47
+
48
+ assert_equal({'name' => 'zoo'}, first_action.input)
49
+ assert_equal({'id' => '123'}, first_action.output)
50
+ assert_equal({'name' => 'foo'}, second_action.input)
51
+ assert_equal({'id' => '456'}, second_action.output)
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,108 @@
1
+ require 'test_helper'
2
+
3
+ module Dynflow
4
+
5
+ describe Dispatcher do
6
+ class Promotion < Action
7
+
8
+ def plan(repo_names, package_names)
9
+ repo_names.each do |repo_name|
10
+ plan_action(CloneRepo, {'name' => repo_name})
11
+ end
12
+
13
+ package_names.each do |package_name|
14
+ plan_action(ClonePackage, {'name' => package_name})
15
+ end
16
+
17
+ plan_self('actions' => repo_names.size + package_names.size)
18
+ end
19
+
20
+ input_format do
21
+ param :actions, Integer
22
+ end
23
+
24
+ end
25
+
26
+ class PromotionObserver < Action
27
+
28
+ def self.subscribe
29
+ Promotion
30
+ end
31
+
32
+ end
33
+
34
+ class CloneRepo < Action
35
+
36
+ input_format do
37
+ param :name, String
38
+ end
39
+
40
+ output_format do
41
+ param :id, String
42
+ end
43
+
44
+ end
45
+
46
+ class ClonePackage < Action
47
+
48
+ input_format do
49
+ param :name, String
50
+ end
51
+
52
+ output_format do
53
+ param :id, String
54
+ end
55
+
56
+ end
57
+
58
+ class UpdateIndex < Action
59
+
60
+ def self.subscribe
61
+ ClonePackage
62
+ end
63
+
64
+ def plan(input)
65
+ plan_action(YetAnotherAction, {'hello' => 'world'})
66
+ super
67
+ end
68
+
69
+ output_format do
70
+ param :indexed_name, String
71
+ end
72
+
73
+ end
74
+
75
+ class YetAnotherAction < Action
76
+
77
+ input_format do
78
+ param :name, String
79
+ param :hello, String
80
+ end
81
+
82
+ output_format do
83
+ param :hello, String
84
+ end
85
+
86
+ def plan(arg)
87
+ plan_self(input.merge(arg))
88
+ end
89
+
90
+ end
91
+
92
+ it "builds the execution plan" do
93
+ execution_plan = Promotion.plan(['zoo', 'foo'], ['elephant'])
94
+ expected_plan =
95
+ [
96
+ [CloneRepo, {'name' => 'zoo'}],
97
+ [CloneRepo, {'name' => 'foo'}],
98
+ [ClonePackage, {'name' => 'elephant'}],
99
+ [YetAnotherAction, {'name' => 'elephant', 'hello' => 'world'}],
100
+ [UpdateIndex, {'name' => 'elephant'}],
101
+ [Promotion, {'actions' => 3 }],
102
+ [PromotionObserver, {'actions' => 3 }]
103
+ ]
104
+ execution_plan.must_equal expected_plan
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,83 @@
1
+ require 'test/unit'
2
+ require 'minitest/spec'
3
+ require 'dynflow'
4
+
5
+ BUS_IMPL = Dynflow::Bus::MemoryBus
6
+
7
+ class TestBus < BUS_IMPL
8
+
9
+ def initialize(expected_scenario)
10
+ super()
11
+ @expected_scenario = expected_scenario
12
+ end
13
+
14
+ def process(action_class, input, output = nil, stub = true)
15
+ expected = @expected_scenario.shift
16
+ if action_class == TestScenarioFinalizer || !stub || output
17
+ return super(action_class, input, output)
18
+ elsif action_class.name == expected[:action_class].name && input == expected[:input]
19
+ return action_class.new(expected[:input], expected[:output])
20
+ else
21
+ raise "Unexpected input. Expected #{expected[:action_class]} #{expected[:input].inspect}, got #{action_class} #{input.inspect}"
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ class TestScenarioFinalizer < Dynflow::Action
28
+
29
+ class << self
30
+
31
+ def recorded_outputs
32
+ @recorded_outputs
33
+ end
34
+
35
+ def init_recorded_outputs
36
+ @recorded_outputs = []
37
+ end
38
+
39
+ def save_recorded_outputs(recorded_outputs)
40
+ @recorded_outputs = recorded_outputs
41
+ end
42
+
43
+ end
44
+
45
+ def finalize(outputs)
46
+ self.class.save_recorded_outputs(outputs)
47
+ end
48
+
49
+ end
50
+
51
+ class BusTestCase < Test::Unit::TestCase
52
+
53
+ def setup
54
+ @expected_scenario = []
55
+ end
56
+
57
+ def expect_input(action_class, input, output)
58
+ @expected_scenario << {
59
+ :action_class => action_class,
60
+ :input => input,
61
+ :output => output
62
+ }
63
+ end
64
+
65
+ def assert_scenario
66
+ Dynflow::Bus.impl = TestBus.new(@expected_scenario)
67
+ event_outputs = nil
68
+ TestScenarioFinalizer.init_recorded_outputs
69
+ execution_plan = self.execution_plan
70
+ execution_plan << [TestScenarioFinalizer, {}]
71
+ Dynflow::Bus.trigger(execution_plan)
72
+ return TestScenarioFinalizer.recorded_outputs
73
+ end
74
+ end
75
+
76
+ class ParticipantTestCase < Test::Unit::TestCase
77
+
78
+ def run_action(action_class, input)
79
+ Dynflow::Bus.impl = Dynflow::Bus.new
80
+ output = Dynflow::Bus.process(action_class, input)
81
+ return output
82
+ end
83
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynflow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ivan Necas
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-04-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
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
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: apipie-params
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Generate and executed workflows dynamically based on input data and leave
79
+ it open for others to jump into it as well
80
+ email:
81
+ - inecas@redhat.com
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - MIT-LICENSE
89
+ - README.md
90
+ - Rakefile
91
+ - dynflow.gemspec
92
+ - examples/events.rb
93
+ - examples/workflow.rb
94
+ - lib/dynflow.rb
95
+ - lib/dynflow/action.rb
96
+ - lib/dynflow/bus.rb
97
+ - lib/dynflow/dispatcher.rb
98
+ - lib/dynflow/logger.rb
99
+ - lib/dynflow/message.rb
100
+ - lib/dynflow/orch_request.rb
101
+ - lib/dynflow/orch_response.rb
102
+ - lib/dynflow/version.rb
103
+ - test/action_test.rb
104
+ - test/bus_test.rb
105
+ - test/dispatcher_test.rb
106
+ - test/test_helper.rb
107
+ homepage: http://github.com/iNecas/eventum
108
+ licenses: []
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 1.8.25
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: DYNamic workFLOW engine
131
+ test_files:
132
+ - test/action_test.rb
133
+ - test/bus_test.rb
134
+ - test/dispatcher_test.rb
135
+ - test/test_helper.rb