standard_procedure_operations 0.5.3 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +163 -474
- data/app/jobs/operations/delete_old_task_job.rb +5 -0
- data/app/jobs/operations/wake_task_job.rb +5 -0
- data/app/models/concerns/operations/participant.rb +4 -9
- data/app/models/operations/task/index.rb +22 -0
- data/app/models/operations/task/plan/action_handler.rb +20 -0
- data/app/models/operations/task/plan/decision_handler.rb +40 -0
- data/app/models/operations/task/plan/interaction_handler.rb +20 -0
- data/app/models/operations/task/plan/result_handler.rb +9 -0
- data/app/models/operations/task/{state_management → plan}/wait_handler.rb +8 -6
- data/app/models/operations/task/plan.rb +70 -0
- data/app/models/operations/task/runner.rb +34 -0
- data/app/models/operations/task.rb +50 -68
- data/app/models/operations/task_participant.rb +2 -5
- data/db/migrate/20250701190516_rename_existing_operations_tables.rb +19 -0
- data/db/migrate/20250701190716_create_new_operations_tasks.rb +20 -0
- data/db/migrate/20250702113801_create_task_participants.rb +10 -0
- data/lib/operations/invalid_state.rb +2 -0
- data/lib/operations/version.rb +1 -1
- data/lib/operations.rb +2 -3
- data/lib/tasks/operations_tasks.rake +3 -3
- metadata +17 -20
- data/app/jobs/operations/task_runner_job.rb +0 -11
- data/app/models/operations/task/background.rb +0 -39
- data/app/models/operations/task/data_carrier.rb +0 -18
- data/app/models/operations/task/deletion.rb +0 -17
- data/app/models/operations/task/exports.rb +0 -45
- data/app/models/operations/task/input_validation.rb +0 -17
- data/app/models/operations/task/state_management/action_handler.rb +0 -17
- data/app/models/operations/task/state_management/completion_handler.rb +0 -14
- data/app/models/operations/task/state_management/decision_handler.rb +0 -49
- data/app/models/operations/task/state_management.rb +0 -39
- data/app/models/operations/task/testing.rb +0 -62
- data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
- data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
- data/db/migrate/20250403075414_add_becomes_zombie_at_field.rb +0 -6
- data/lib/operations/cannot_wait_in_foreground.rb +0 -2
- data/lib/operations/exporters/svg.rb +0 -399
@@ -1,18 +0,0 @@
|
|
1
|
-
class Operations::Task::DataCarrier < OpenStruct
|
2
|
-
def fail_with(message) = task.fail_with(message)
|
3
|
-
|
4
|
-
def call(sub_task_class, **data, &result_handler) = task.call(sub_task_class, **data, &result_handler)
|
5
|
-
|
6
|
-
def start(sub_task_class, **data, &result_handler) = task.start(sub_task_class, **data, &result_handler)
|
7
|
-
|
8
|
-
def go_to(state, data = nil) = task.go_to state, data || self
|
9
|
-
|
10
|
-
def complete(results) = task.complete(results)
|
11
|
-
|
12
|
-
def inputs(*names)
|
13
|
-
missing_inputs = (names.map(&:to_sym) - to_h.keys)
|
14
|
-
raise ArgumentError.new("Missing inputs: #{missing_inputs.join(", ")}") if missing_inputs.any?
|
15
|
-
end
|
16
|
-
|
17
|
-
def optional(*names) = nil
|
18
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Operations::Task::Deletion
|
2
|
-
extend ActiveSupport::Concern
|
3
|
-
|
4
|
-
included do
|
5
|
-
scope :for_deletion, -> { where(delete_at: ..Time.now.utc) }
|
6
|
-
attribute :delete_at, :datetime, default: -> { deletes_after.from_now.utc }
|
7
|
-
validates :delete_at, presence: true
|
8
|
-
end
|
9
|
-
|
10
|
-
class_methods do
|
11
|
-
def delete_after(value) = @@deletes_after = value
|
12
|
-
|
13
|
-
def deletes_after = @@deletes_after ||= 90.days
|
14
|
-
|
15
|
-
def delete_expired = for_deletion.destroy_all
|
16
|
-
end
|
17
|
-
end
|
@@ -1,45 +0,0 @@
|
|
1
|
-
module Operations::Task::Exports
|
2
|
-
extend ActiveSupport::Concern
|
3
|
-
class_methods do
|
4
|
-
# Returns a hash representation of the task's structure
|
5
|
-
# Useful for exporting to different formats (e.g., GraphViz)
|
6
|
-
def to_h
|
7
|
-
{name: name, initial_state: initial_state, inputs: required_inputs, optional_inputs: optional_inputs, states: state_handlers.transform_values { |handler| handler_to_h(handler) }}
|
8
|
-
end
|
9
|
-
|
10
|
-
def handler_to_h(handler)
|
11
|
-
case handler
|
12
|
-
when Operations::Task::StateManagement::DecisionHandler
|
13
|
-
{type: :decision, transitions: decision_transitions(handler), inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
|
14
|
-
when Operations::Task::StateManagement::ActionHandler
|
15
|
-
{type: :action, next_state: handler.next_state, inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
|
16
|
-
when Operations::Task::StateManagement::WaitHandler
|
17
|
-
{type: :wait, transitions: wait_transitions(handler), inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
|
18
|
-
when Operations::Task::StateManagement::CompletionHandler
|
19
|
-
{type: :result, inputs: extract_inputs(handler), optional_inputs: extract_optional_inputs(handler)}
|
20
|
-
else
|
21
|
-
{type: :unknown}
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def extract_inputs(handler)
|
26
|
-
handler.instance_variable_defined?(:@required_inputs) ? handler.instance_variable_get(:@required_inputs) : []
|
27
|
-
end
|
28
|
-
|
29
|
-
def extract_optional_inputs(handler)
|
30
|
-
handler.instance_variable_defined?(:@optional_inputs) ? handler.instance_variable_get(:@optional_inputs) : []
|
31
|
-
end
|
32
|
-
|
33
|
-
def decision_transitions(handler)
|
34
|
-
if handler.instance_variable_defined?(:@true_state) && handler.instance_variable_defined?(:@false_state)
|
35
|
-
{"true" => handler.instance_variable_get(:@true_state), "false" => handler.instance_variable_get(:@false_state)}
|
36
|
-
else
|
37
|
-
handler.instance_variable_get(:@destinations).map.with_index { |dest, i| [:"condition_#{i}", dest] }.to_h
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def wait_transitions(handler)
|
42
|
-
handler.instance_variable_get(:@destinations).map.with_index { |dest, i| [:"condition_#{i}", dest] }.to_h
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module Operations::Task::InputValidation
|
2
|
-
def inputs(*names) = @required_inputs = names.map(&:to_sym)
|
3
|
-
|
4
|
-
def optional(*names) = @optional_inputs = names.map(&:to_sym)
|
5
|
-
|
6
|
-
def optional_inputs = @optional_inputs ||= []
|
7
|
-
|
8
|
-
def required_inputs = @required_inputs ||= []
|
9
|
-
|
10
|
-
def required_inputs_are_present_in?(hash) = missing_inputs_from(hash).empty?
|
11
|
-
|
12
|
-
def missing_inputs_from(hash) = (required_inputs - hash.keys.map(&:to_sym))
|
13
|
-
|
14
|
-
def validate_inputs! hash
|
15
|
-
raise ArgumentError, "Missing inputs: #{missing_inputs_from(hash).join(", ")}" unless required_inputs_are_present_in?(hash)
|
16
|
-
end
|
17
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
class Operations::Task::StateManagement::ActionHandler
|
2
|
-
attr_accessor :next_state
|
3
|
-
|
4
|
-
def initialize name, &action
|
5
|
-
@name = name.to_sym
|
6
|
-
@required_inputs = []
|
7
|
-
@optional_inputs = []
|
8
|
-
@action = action
|
9
|
-
@next_state = nil
|
10
|
-
end
|
11
|
-
|
12
|
-
def call(task, data)
|
13
|
-
data.instance_exec(&@action).tap do |result|
|
14
|
-
data.go_to @next_state unless @next_state.nil?
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
class Operations::Task::StateManagement::CompletionHandler
|
2
|
-
def initialize name, inputs = [], optional = [], &handler
|
3
|
-
@name = name.to_sym
|
4
|
-
@required_inputs = inputs
|
5
|
-
@optional_inputs = optional
|
6
|
-
@handler = handler
|
7
|
-
end
|
8
|
-
|
9
|
-
def call(task, data)
|
10
|
-
results = OpenStruct.new
|
11
|
-
data.instance_exec(results, &@handler) unless @handler.nil?
|
12
|
-
data.complete(results)
|
13
|
-
end
|
14
|
-
end
|
@@ -1,49 +0,0 @@
|
|
1
|
-
class Operations::Task::StateManagement::DecisionHandler
|
2
|
-
include Operations::Task::InputValidation
|
3
|
-
|
4
|
-
def initialize name, &config
|
5
|
-
@name = name.to_sym
|
6
|
-
@conditions = []
|
7
|
-
@destinations = []
|
8
|
-
@true_state = nil
|
9
|
-
@false_state = nil
|
10
|
-
instance_eval(&config)
|
11
|
-
end
|
12
|
-
|
13
|
-
def condition(destination = nil, options = {}, &condition)
|
14
|
-
@conditions << condition
|
15
|
-
@destinations << destination if destination
|
16
|
-
@condition_labels ||= {}
|
17
|
-
condition_index = @conditions.size - 1
|
18
|
-
@condition_labels[condition_index] = options[:label] if options[:label]
|
19
|
-
end
|
20
|
-
|
21
|
-
def go_to(destination) = @destinations << destination
|
22
|
-
|
23
|
-
def condition_labels
|
24
|
-
@condition_labels ||= {}
|
25
|
-
end
|
26
|
-
|
27
|
-
def if_true(state = nil, &handler) = @true_state = state || handler
|
28
|
-
|
29
|
-
def if_false(state = nil, &handler) = @false_state = state || handler
|
30
|
-
|
31
|
-
def call(task, data)
|
32
|
-
validate_inputs! data.to_h
|
33
|
-
has_true_false_handlers? ? handle_single_condition(task, data) : handle_multiple_conditions(task, data)
|
34
|
-
end
|
35
|
-
|
36
|
-
private def has_true_false_handlers? = !@true_state.nil? || !@false_state.nil?
|
37
|
-
|
38
|
-
private def handle_single_condition(task, data)
|
39
|
-
next_state = data.instance_eval(&@conditions.first) ? @true_state : @false_state
|
40
|
-
next_state.respond_to?(:call) ? data.instance_eval(&next_state) : data.go_to(next_state, data)
|
41
|
-
end
|
42
|
-
|
43
|
-
private def handle_multiple_conditions(task, data)
|
44
|
-
condition = @conditions.find { |condition| data.instance_eval(&condition) }
|
45
|
-
raise Operations::NoDecision.new("No conditions matched #{@name}") if condition.nil?
|
46
|
-
index = @conditions.index condition
|
47
|
-
data.go_to(@destinations[index])
|
48
|
-
end
|
49
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
module Operations::Task::StateManagement
|
2
|
-
extend ActiveSupport::Concern
|
3
|
-
|
4
|
-
included do
|
5
|
-
attribute :state, :string
|
6
|
-
validate :state_is_valid
|
7
|
-
end
|
8
|
-
|
9
|
-
class_methods do
|
10
|
-
def starts_with(value) = @initial_state = value.to_sym
|
11
|
-
|
12
|
-
def initial_state = @initial_state
|
13
|
-
|
14
|
-
def decision(name, &config) = state_handlers[name.to_sym] = DecisionHandler.new(name, &config)
|
15
|
-
|
16
|
-
def action(name, &handler) = state_handlers[name.to_sym] = ActionHandler.new(name, &handler)
|
17
|
-
|
18
|
-
def wait_until(name, &config) = state_handlers[name.to_sym] = WaitHandler.new(name, &config)
|
19
|
-
|
20
|
-
def result(name, inputs: [], optional: [], &results) = state_handlers[name.to_sym] = CompletionHandler.new(name, inputs, optional, &results)
|
21
|
-
|
22
|
-
def go_to(state)
|
23
|
-
# Get the most recently defined action handler
|
24
|
-
last_action = state_handlers.values.reverse.find { |h| h.is_a?(ActionHandler) }
|
25
|
-
raise ArgumentError, "No action handler defined yet" unless last_action
|
26
|
-
|
27
|
-
last_action.next_state = state.to_sym
|
28
|
-
end
|
29
|
-
|
30
|
-
def state_handlers = @state_handlers ||= {}
|
31
|
-
|
32
|
-
def handler_for(state) = state_handlers[state.to_sym]
|
33
|
-
end
|
34
|
-
|
35
|
-
private def handler_for(state) = self.class.handler_for(state.to_sym)
|
36
|
-
private def state_is_valid
|
37
|
-
errors.add :state, :invalid if state.blank? || handler_for(state.to_sym).nil?
|
38
|
-
end
|
39
|
-
end
|
@@ -1,62 +0,0 @@
|
|
1
|
-
module Operations::Task::Testing
|
2
|
-
extend ActiveSupport::Concern
|
3
|
-
|
4
|
-
class_methods do
|
5
|
-
def handling state, background: false, **data, &block
|
6
|
-
# Create a task specifically for testing - avoid serialization issues
|
7
|
-
task = new(state: state, background: background)
|
8
|
-
# Use our own test-specific data carrier so we can examine results
|
9
|
-
data = TestResultCarrier.new(data.merge(task: task))
|
10
|
-
|
11
|
-
# Testing doesn't use the database, so handle serialization by overriding task's go_to
|
12
|
-
# to avoid serialization errors
|
13
|
-
def task.go_to(state, data = {}, message: nil)
|
14
|
-
self.state = state
|
15
|
-
# Don't call super to avoid serialization
|
16
|
-
end
|
17
|
-
|
18
|
-
handler_for(state).call(task, data)
|
19
|
-
data.completion_results.nil? ? block.call(data) : block.call(data.completion_results)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
# Instead of extending DataCarrier (which no longer has go_to),
|
24
|
-
# create a new class with similar functionality but keeps the go_to method for testing
|
25
|
-
class TestResultCarrier < Operations::Task::DataCarrier
|
26
|
-
def go_to(state, message = nil)
|
27
|
-
self.next_state = state
|
28
|
-
self.status_message = message || next_state.to_s
|
29
|
-
end
|
30
|
-
|
31
|
-
def fail_with(message)
|
32
|
-
self.failure_message = message
|
33
|
-
end
|
34
|
-
|
35
|
-
def inputs(*names)
|
36
|
-
missing_inputs = (names.map(&:to_sym) - to_h.keys)
|
37
|
-
raise ArgumentError.new("Missing inputs: #{missing_inputs.join(", ")}") if missing_inputs.any?
|
38
|
-
end
|
39
|
-
|
40
|
-
def optional(*names) = nil
|
41
|
-
|
42
|
-
def call(sub_task_class, **data, &result_handler)
|
43
|
-
record_sub_task sub_task_class
|
44
|
-
super
|
45
|
-
end
|
46
|
-
|
47
|
-
def start(sub_task_class, **data, &result_handler)
|
48
|
-
record_sub_task sub_task_class
|
49
|
-
# Just record the sub_task for testing, don't actually start it
|
50
|
-
nil
|
51
|
-
end
|
52
|
-
|
53
|
-
def complete(results)
|
54
|
-
self.completion_results = results
|
55
|
-
end
|
56
|
-
|
57
|
-
private def record_sub_task sub_task_class
|
58
|
-
self.sub_tasks ||= []
|
59
|
-
self.sub_tasks << sub_task_class
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
class CreateOperationsTasks < ActiveRecord::Migration[7.1]
|
2
|
-
def change
|
3
|
-
create_table :operations_tasks do |t|
|
4
|
-
t.string :type
|
5
|
-
t.integer :status, default: 0, null: false
|
6
|
-
t.string :state, null: false
|
7
|
-
t.string :status_message, default: "", null: false
|
8
|
-
t.text :data
|
9
|
-
t.text :results
|
10
|
-
t.boolean :background, default: false, null: false
|
11
|
-
t.datetime :delete_at, null: false, index: true
|
12
|
-
t.timestamps
|
13
|
-
end
|
14
|
-
|
15
|
-
add_index :operations_tasks, [:type, :status]
|
16
|
-
end
|
17
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
class CreateOperationsTaskParticipants < ActiveRecord::Migration[7.1]
|
2
|
-
def change
|
3
|
-
create_table :operations_task_participants do |t|
|
4
|
-
t.references :task, null: false, foreign_key: {to_table: :operations_tasks}
|
5
|
-
t.references :participant, polymorphic: true, null: false
|
6
|
-
t.string :role, null: false
|
7
|
-
t.string :context, null: false, default: "data"
|
8
|
-
t.timestamps
|
9
|
-
end
|
10
|
-
|
11
|
-
add_index :operations_task_participants, [:task_id, :participant_type, :participant_id, :role, :context],
|
12
|
-
name: "index_operations_task_participants_on_full_identity",
|
13
|
-
unique: true
|
14
|
-
end
|
15
|
-
end
|