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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +163 -474
  3. data/app/jobs/operations/delete_old_task_job.rb +5 -0
  4. data/app/jobs/operations/wake_task_job.rb +5 -0
  5. data/app/models/concerns/operations/participant.rb +4 -9
  6. data/app/models/operations/task/index.rb +22 -0
  7. data/app/models/operations/task/plan/action_handler.rb +20 -0
  8. data/app/models/operations/task/plan/decision_handler.rb +40 -0
  9. data/app/models/operations/task/plan/interaction_handler.rb +20 -0
  10. data/app/models/operations/task/plan/result_handler.rb +9 -0
  11. data/app/models/operations/task/{state_management → plan}/wait_handler.rb +8 -6
  12. data/app/models/operations/task/plan.rb +70 -0
  13. data/app/models/operations/task/runner.rb +34 -0
  14. data/app/models/operations/task.rb +50 -68
  15. data/app/models/operations/task_participant.rb +2 -5
  16. data/db/migrate/20250701190516_rename_existing_operations_tables.rb +19 -0
  17. data/db/migrate/20250701190716_create_new_operations_tasks.rb +20 -0
  18. data/db/migrate/20250702113801_create_task_participants.rb +10 -0
  19. data/lib/operations/invalid_state.rb +2 -0
  20. data/lib/operations/version.rb +1 -1
  21. data/lib/operations.rb +2 -3
  22. data/lib/tasks/operations_tasks.rake +3 -3
  23. metadata +17 -20
  24. data/app/jobs/operations/task_runner_job.rb +0 -11
  25. data/app/models/operations/task/background.rb +0 -39
  26. data/app/models/operations/task/data_carrier.rb +0 -18
  27. data/app/models/operations/task/deletion.rb +0 -17
  28. data/app/models/operations/task/exports.rb +0 -45
  29. data/app/models/operations/task/input_validation.rb +0 -17
  30. data/app/models/operations/task/state_management/action_handler.rb +0 -17
  31. data/app/models/operations/task/state_management/completion_handler.rb +0 -14
  32. data/app/models/operations/task/state_management/decision_handler.rb +0 -49
  33. data/app/models/operations/task/state_management.rb +0 -39
  34. data/app/models/operations/task/testing.rb +0 -62
  35. data/db/migrate/20250127160616_create_operations_tasks.rb +0 -17
  36. data/db/migrate/20250309160616_create_operations_task_participants.rb +0 -15
  37. data/db/migrate/20250403075414_add_becomes_zombie_at_field.rb +0 -6
  38. data/lib/operations/cannot_wait_in_foreground.rb +0 -2
  39. 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
@@ -1,6 +0,0 @@
1
- class AddBecomesZombieAtField < ActiveRecord::Migration[8.0]
2
- def change
3
- add_column :operations_tasks, :becomes_zombie_at, :datetime, null: true
4
- add_index :operations_tasks, :becomes_zombie_at
5
- end
6
- end
@@ -1,2 +0,0 @@
1
- class Operations::CannotWaitInForeground < Operations::Error
2
- end