src/derails/active_job/continuation/step.rb
# frozen_string_literal: true

module ActiveJob
  class Continuation
    # = Active Job Continuation Step
    #
    # Represents a step within a continuable job.
    #
    # When a step is completed, it is recorded in the job's continuation state.
    # If the job is interrupted, it will be resumed from after the last completed step.
    #
    # Steps also have an optional cursor that can be used to track progress within the step.
    # If a job is interrupted during a step, the cursor will be saved and passed back when
    # the job is resumed.
    #
    # It is the responsibility of the code in the step to use the cursor correctly to resume
    # from where it left off.
    class Step
      # The name of the step.
      attr_reader :name

      # The cursor for the step.
      attr_reader :cursor

      def initialize(name, cursor, job:, resumed:)
        @name = name.to_sym
        @initial_cursor = cursor
        @cursor = cursor
        @resumed = resumed
        @job = job
      end

      # Check if the job should be interrupted, and if so raise an Interrupt exception.
      # The job will be requeued for retry.
      def checkpoint!
        job.checkpoint!
      end

      # Set the cursor and interrupt the job if necessary.
      def set!(cursor)
        @cursor = cursor
        checkpoint!
      end

      # Advance the cursor from the current or supplied value
      #
      # The cursor will be advanced by calling the +succ+ method on the cursor.
      # An UnadvanceableCursorError error will be raised if the cursor does not implement +succ+.
      def advance!(from: nil)
        from = cursor if from.nil?

        begin
          to = from.succ
        rescue NoMethodError
          raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
        end

        set! to
      end

      # Has this step been resumed from a previous job execution?
      def resumed?
        @resumed
      end

      # Has the cursor been advanced during this job execution?
      def advanced?
        initial_cursor != cursor
      end

      def to_a
        [ name.to_s, cursor ]
      end

      def description
        "at '#{name}', cursor '#{cursor.inspect}'"
      end

      private
        attr_reader :initial_cursor, :job
    end
  end
end
