dynflow 0.1.0 → 0.2.0

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.
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