dynflow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,160 @@
1
+ require 'sequel/no_core_ext' # to avoid sequel ~> 3.0 coliding with ActiveRecord
2
+ require 'multi_json'
3
+
4
+ module Dynflow
5
+ module PersistenceAdapters
6
+
7
+ Sequel.extension :migration
8
+
9
+ class Sequel < Abstract
10
+ include Algebrick::TypeCheck
11
+
12
+ attr_reader :db
13
+
14
+ def pagination?
15
+ true
16
+ end
17
+
18
+ def filtering_by
19
+ META_DATA.fetch :execution_plan
20
+ end
21
+
22
+ def ordering_by
23
+ META_DATA.fetch :execution_plan
24
+ end
25
+
26
+ META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
27
+ action: [],
28
+ step: %w(state started_at ended_at real_time execution_time action_id) }
29
+
30
+ def initialize(db_path)
31
+ @db = initialize_db db_path
32
+ migrate_db
33
+ end
34
+
35
+ def find_execution_plans(options = {})
36
+ data_set = filter(order(paginate(table(:execution_plan), options), options), options)
37
+
38
+ data_set.map do |record|
39
+ HashWithIndifferentAccess.new(MultiJson.load(record[:data]))
40
+ end
41
+ end
42
+
43
+ def load_execution_plan(execution_plan_id)
44
+ load :execution_plan, uuid: execution_plan_id
45
+ end
46
+
47
+ def save_execution_plan(execution_plan_id, value)
48
+ save :execution_plan, { uuid: execution_plan_id }, value
49
+ end
50
+
51
+ def load_step(execution_plan_id, step_id)
52
+ load :step, execution_plan_uuid: execution_plan_id, id: step_id
53
+ end
54
+
55
+ def save_step(execution_plan_id, step_id, value)
56
+ save :step, { execution_plan_uuid: execution_plan_id, id: step_id }, value
57
+ end
58
+
59
+ def load_action(execution_plan_id, action_id)
60
+ load :action, execution_plan_uuid: execution_plan_id, id: action_id
61
+ end
62
+
63
+ def save_action(execution_plan_id, action_id, value)
64
+ save :action, { execution_plan_uuid: execution_plan_id, id: action_id }, value
65
+ end
66
+
67
+ def to_hash
68
+ { execution_plans: table(:execution_plan).all,
69
+ steps: table(:step).all,
70
+ actions: table(:action).all }
71
+ end
72
+
73
+ private
74
+
75
+ TABLES = { execution_plan: :dynflow_execution_plans,
76
+ action: :dynflow_actions,
77
+ step: :dynflow_steps }
78
+
79
+ def table(which)
80
+ db[TABLES.fetch(which)]
81
+ end
82
+
83
+ def initialize_db(db_path)
84
+ ::Sequel.connect db_path
85
+ end
86
+
87
+ def self.migrations_path
88
+ File.expand_path('../sequel_migrations', __FILE__)
89
+ end
90
+
91
+ def migrate_db
92
+ ::Sequel::Migrator.run(db, self.class.migrations_path, table: 'dynflow_schema_info')
93
+ end
94
+
95
+ def save(what, condition, value)
96
+ table = table(what)
97
+ existing_record = table.first condition
98
+
99
+ if value
100
+ value = value.with_indifferent_access
101
+ record = existing_record || condition
102
+ record[:data] = MultiJson.dump Type!(value, Hash)
103
+ meta_data = META_DATA.fetch(what).inject({}) { |h, k| h.update k.to_sym => value.fetch(k) }
104
+ record.merge! meta_data
105
+ record.each { |k, v| record[k] = v.to_s if v.is_a? Symbol }
106
+
107
+ if existing_record
108
+ table.where(condition).update(record)
109
+ else
110
+ table.insert record
111
+ end
112
+
113
+ else
114
+ existing_record and table.where(condition).delete
115
+ end
116
+ value
117
+ end
118
+
119
+ def load(what, condition)
120
+ table = table(what)
121
+ if (record = table.first(condition.symbolize_keys))
122
+ HashWithIndifferentAccess.new MultiJson.load(record[:data])
123
+ else
124
+ raise KeyError, "searching: #{what} by: #{condition.inspect}"
125
+ end
126
+ end
127
+
128
+ def paginate(data_set, options)
129
+ page = Integer(options[:page] || 0)
130
+ per_page = Integer(options[:per_page] || 20)
131
+
132
+ if page
133
+ data_set.limit per_page, per_page * page
134
+ else
135
+ data_set
136
+ end
137
+ end
138
+
139
+ def order(data_set, options)
140
+ order_by = (options[:order_by] || :started_at).to_s
141
+ unless META_DATA.fetch(:execution_plan).include? order_by
142
+ raise ArgumentError, "unknown column #{order_by.inspect}"
143
+ end
144
+ order_by = order_by.to_sym
145
+ data_set.order_by options[:desc] ? ::Sequel.desc(order_by) : order_by
146
+ end
147
+
148
+ def filter(data_set, options)
149
+ filters = Type! options[:filters], NilClass, Hash
150
+ return data_set if filters.nil?
151
+
152
+ unless (unknown = filters.keys - META_DATA.fetch(:execution_plan)).empty?
153
+ raise ArgumentError, "unkown columns: #{unknown.inspect}"
154
+ end
155
+
156
+ data_set.where filters.symbolize_keys
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,52 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table(:dynflow_execution_plans) do
4
+ column :uuid, String, primary_key: true, size: 36, fixed: true
5
+ index :uuid, :unique => true
6
+
7
+ column :data, String, text: true
8
+
9
+ column :state, String
10
+ column :result, String
11
+ column :started_at, Time
12
+ column :ended_at, Time
13
+ column :real_time, Float
14
+ column :execution_time, Float
15
+ end
16
+
17
+ create_table(:dynflow_actions) do
18
+ foreign_key :execution_plan_uuid, :dynflow_execution_plans, type: String, size: 36, fixed: true
19
+ index :execution_plan_uuid
20
+ column :id, Fixnum
21
+ primary_key [:execution_plan_uuid, :id]
22
+ index [:execution_plan_uuid, :id], :unique => true
23
+
24
+ column :data, String, text: true
25
+ end
26
+
27
+ create_table(:dynflow_steps) do
28
+ foreign_key :execution_plan_uuid, :dynflow_execution_plans, type: String, size: 36, fixed: true
29
+ index :execution_plan_uuid
30
+ column :id, Fixnum
31
+ primary_key [:execution_plan_uuid, :id]
32
+ index [:execution_plan_uuid, :id], :unique => true
33
+ column :action_id, Fixnum
34
+ foreign_key [:execution_plan_uuid, :action_id], :dynflow_actions
35
+ index [:execution_plan_uuid, :action_id]
36
+
37
+ column :data, String, text: true
38
+
39
+ column :state, String
40
+ column :started_at, Time
41
+ column :ended_at, Time
42
+ column :real_time, Float
43
+ column :execution_time, Float
44
+ end
45
+ end
46
+
47
+ down do
48
+ drop_table(:dynflow_steps)
49
+ drop_table(:dynflow_actions)
50
+ drop_table(:dynflow_execution_plans)
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ module Dynflow
2
+ class Serializable
3
+ def self.from_hash(hash, *args)
4
+ check_class_key_present hash
5
+ hash[:class].constantize.new_from_hash(hash, *args)
6
+ end
7
+
8
+ def to_hash
9
+ raise NotImplementedError
10
+ end
11
+
12
+ # @api private
13
+ def self.new_from_hash(hash, *args)
14
+ raise NotImplementedError
15
+ # new ...
16
+ end
17
+
18
+ def self.check_class_matching(hash)
19
+ check_class_key_present hash
20
+ unless self.to_s == hash[:class]
21
+ raise ArgumentError, "class mismatch #{hash[:class]} != #{self}"
22
+ end
23
+ end
24
+
25
+ def self.check_class_key_present(hash)
26
+ raise ArgumentError, 'missing :class' unless hash[:class]
27
+ end
28
+
29
+ private_class_method :check_class_matching, :check_class_key_present
30
+
31
+ private
32
+
33
+ def recursive_to_hash(value)
34
+ case value
35
+ when Numeric, String, Symbol, TrueClass, FalseClass, NilClass
36
+ value
37
+ when Array
38
+ value.map { |v| recursive_to_hash v }
39
+ when Hash
40
+ value.inject({}) { |h, (k, v)| h.update k => recursive_to_hash(v) }
41
+ else
42
+ value.to_hash
43
+ end
44
+ end
45
+
46
+ def self.string_to_time(string)
47
+ return if string.nil?
48
+ _, year, month, day, hour, min, sec = */(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.match(string)
49
+ Time.new(year.to_i, month.to_i, day.to_i, hour.to_i, min.to_i, sec.to_i)
50
+ end
51
+
52
+ def time_to_str(time)
53
+ return if time.nil?
54
+ Type! time, Time
55
+ time.strftime '%Y-%m-%d %H:%M:%S'
56
+ end
57
+
58
+ def self.hash_to_error(hash)
59
+ return nil if hash.nil?
60
+ ExecutionPlan::Steps::Error.from_hash(hash)
61
+ end
62
+
63
+ private_class_method :string_to_time, :hash_to_error
64
+
65
+ end
66
+ end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ class SimpleWorld < World
3
+ def initialize(options_hash = {})
4
+ super options_hash
5
+ at_exit { self.terminate.wait } if options[:auto_terminate]
6
+ # we can check consistency here because SimpleWorld doesn't expect
7
+ # remote executor being in place.
8
+ self.consistency_check
9
+ end
10
+
11
+ def default_options
12
+ super.merge(pool_size: 5,
13
+ persistence_adapter: PersistenceAdapters::Sequel.new('sqlite:/'),
14
+ transaction_adapter: TransactionAdapters::None.new,
15
+ auto_terminate: true)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ module Dynflow
2
+ module Stateful
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def states
9
+ raise NotImplementedError
10
+ end
11
+
12
+ def state_transitions
13
+ raise NotImplementedError
14
+ end
15
+ end
16
+
17
+ def states
18
+ self.class.states
19
+ end
20
+
21
+ def state_transitions
22
+ self.class.state_transitions
23
+ end
24
+
25
+ attr_reader :state
26
+
27
+ def state=(state)
28
+ set_state state, false
29
+ end
30
+
31
+ def set_state(state, skip_transition_check)
32
+ state = state.to_sym if state.is_a?(String) && states.map(&:to_s).include?(state)
33
+ raise "unknown state #{state}" unless states.include? state
34
+ unless self.state.nil? || skip_transition_check || state_transitions.fetch(self.state).include?(state)
35
+ raise "invalid state transition #{self.state} >> #{state} in #{self}"
36
+ end
37
+ @state = state
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ module Dynflow
2
+ module Testing
3
+ extend Algebrick::TypeCheck
4
+
5
+ def self.logger_adapter
6
+ @logger_adapter ||= LoggerAdapters::Simple.new $stdout, 0
7
+ end
8
+
9
+ def self.logger_adapter=(adapter)
10
+ Type! adapter, LoggerAdapters::Abstract
11
+ @logger_adapter = adapter
12
+ end
13
+
14
+ def self.get_id
15
+ @last_id ||= 0
16
+ @last_id += 1
17
+ end
18
+
19
+ require 'dynflow/testing/mimic'
20
+ require 'dynflow/testing/managed_clock'
21
+ require 'dynflow/testing/dummy_world'
22
+ require 'dynflow/testing/dummy_executor'
23
+ require 'dynflow/testing/dummy_execution_plan'
24
+ require 'dynflow/testing/dummy_step'
25
+ require 'dynflow/testing/dummy_planned_action'
26
+ require 'dynflow/testing/assertions'
27
+ require 'dynflow/testing/factories'
28
+
29
+ include Assertions
30
+ include Factories
31
+ end
32
+ end
@@ -0,0 +1,64 @@
1
+ module Dynflow
2
+ module Testing
3
+ module Assertions
4
+ # assert that +assert_actioned_plan+ was planned by +action+ with arguments +plan_input+
5
+ # alternatively plan-input can be asserted with +block+
6
+ def assert_action_planed_with(action, planned_action_class, *plan_input, &block)
7
+ found_classes = assert_action_planed(action, planned_action_class)
8
+ found = found_classes.select do |a|
9
+ if plan_input.empty?
10
+ block.call a.plan_input
11
+ else
12
+ a.plan_input == plan_input
13
+ end
14
+ end
15
+
16
+ assert(!found.empty?,
17
+ "Action #{planned_action_class} with plan_input #{plan_input} was not planned, there were only #{found_classes.map(&:plan_input)}")
18
+ found
19
+ end
20
+
21
+ # assert that +assert_actioned_plan+ was planned by +action+
22
+ def assert_action_planed(action, planned_action_class)
23
+ Type! action, Dynflow::Action::PlanPhase
24
+ Match! action.state, :success
25
+ found = action.execution_plan.planned_plan_steps.
26
+ select { |a| a.is_a?(planned_action_class) }
27
+
28
+ assert(!found.empty?, "Action #{planned_action_class} was not planned")
29
+ found
30
+ end
31
+
32
+ # assert that +action+ has run-phase planned
33
+ def assert_run_phase(action, input = nil, &block)
34
+ Type! action, Dynflow::Action::PlanPhase
35
+ Match! action.state, :success
36
+ action.execution_plan.planned_run_steps.must_include action
37
+ action.input.must_equal input.stringify_keys if input
38
+ block.call input if block
39
+ end
40
+
41
+ # refute that +action+ has run-phase planned
42
+ def refute_run_phase(action)
43
+ Type! action, Dynflow::Action::PlanPhase
44
+ Match! action.state, :success
45
+ action.execution_plan.planned_run_steps.wont_include action
46
+ end
47
+
48
+ # assert that +action+ has finalize-phase planned
49
+ def assert_finalize_phase(action)
50
+ Type! action, Dynflow::Action::PlanPhase
51
+ Match! action.state, :success
52
+ action.execution_plan.planned_finalize_steps.must_include action
53
+ end
54
+
55
+ # refute that +action+ has finalize-phase planned
56
+ def refute_finalize_phase(action)
57
+ Type! action, Dynflow::Action::PlanPhase
58
+ Match! action.state, :success
59
+ action.execution_plan.planned_finalize_steps.wont_include action
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,40 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyExecutionPlan
4
+ extend Mimic
5
+ mimic! ExecutionPlan
6
+
7
+ attr_reader :id, :planned_plan_steps, :planned_run_steps, :planned_finalize_steps
8
+
9
+ def initialize
10
+ @id = Testing.get_id
11
+ @planned_plan_steps = []
12
+ @planned_run_steps = []
13
+ @planned_finalize_steps = []
14
+ end
15
+
16
+ def world
17
+ @world ||= DummyWorld.new
18
+ end
19
+
20
+ def add_run_step(action)
21
+ @planned_run_steps << action
22
+ action
23
+ end
24
+
25
+ def add_finalize_step(action)
26
+ @planned_finalize_steps << action
27
+ action
28
+ end
29
+
30
+ def add_plan_step(klass, action)
31
+ @planned_plan_steps << action = DummyPlannedAction.new(klass)
32
+ action
33
+ end
34
+
35
+ def switch_flow(*args, &block)
36
+ block.call
37
+ end
38
+ end
39
+ end
40
+ end