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