module DatashiftJourney::StateMachines::Planner

Mixed into the State Machine class, so your JourneyPlan class has access to these methods and attributes

Public Class Methods

hash_klass() click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 71
def self.hash_klass
  ActiveSupport::HashWithIndifferentAccess
end

Public Instance Methods

branch_sequence(sequence_id, *list) click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 67
def branch_sequence(sequence_id, *list)
  branch_sequence_map.add_or_concat(sequence_id, list)
end
branch_sequence_map() click to toggle source

Key - sequence ID

# File lib/datashift_journey/state_machines/planner.rb, line 24
def branch_sequence_map
  @branch_sequence_map ||= BranchSequenceMap.new
end
init_plan() click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 13
def init_plan
  sequence_list.clear                        # In development context where models get reloaded, this could duplicate
  branch_sequence_map.clear
end
sequence(*list) click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 28
def sequence(*list)
  raise PlannerApiError, 'Empty list passed to sequence - check your MachineBuilder syntax' if list.empty?
  sequence_list << Sequence.new(list.flatten)
end
sequence_list() click to toggle source

The complete, Ordered collection of sequences, used to generate the steps (states) of the plan

# File lib/datashift_journey/state_machines/planner.rb, line 19
def sequence_list
  @sequence_list ||= StateList.new
end
split_on_equality(state, attr_reader, seq_to_target_value_map, _options = {}) click to toggle source

Path splits down different branches, based on values stored on the main Journey model usually collected from from input e.g. radio, text or checkbox

Requires the starting or parent state, and the routing criteria to each target sequence

split_on_equality( :new_or_renew,
                   "what_branch?",             # Helper method on the journey Class
                   branch_1: 'branch_1',
                   branch_2: 'branch_2'
)

target_on_value_map is a hash mapping between the value collected from website and the associated named branch.

if value collected on parent state == stored target state, journey is routed down that branch
# File lib/datashift_journey/state_machines/planner.rb, line 48
def split_on_equality(state, attr_reader, seq_to_target_value_map, _options = {})
  unless seq_to_target_value_map.is_a? Hash
    raise 'BadDefinition - target_on_value_map must be hash map value => associated branch state'
  end

  sequence_list << Sequence.new(state, split: true)

  seq_to_target_value_map.each do |seq_id, trigger_value|
    if branch_sequence_map[seq_id]
      branch_sequence_map[seq_id].entry_state = state
      branch_sequence_map[seq_id].trigger_method = attr_reader
      branch_sequence_map[seq_id].trigger_value = trigger_value
    else
      seq = Sequence.new(nil, id: seq_id, entry_state: state, trigger_method: attr_reader, trigger_value: trigger_value)
      branch_sequence_map.add_branch(seq_id, seq)
    end
  end
end

Protected Instance Methods

build_journey_plan() click to toggle source

Based upon the current sequences, events defined build the Complete Plan, including back and next navigation

# File lib/datashift_journey/state_machines/planner.rb, line 80
def build_journey_plan
  # The Order of sequences should have been preserved as insertion order

  #puts "DEBUG: START PLAN - Processing SEQUENCES\n#{sequence_list.inspect}"

  sequence_list.each_with_index do |sequence, i|
    prev_seq = i.zero? ? EmptySequence.new : sequence_list[i - 1]

    next_seq = sequence_list[i + 1] || EmptySequence.new

    if sequence.split?
      #puts "\nDEBUG: *** BUILDING SPLITTER #{sequence.inspect} (#{i})"
      build_split_sequence_events(sequence, prev_seq, next_seq)
    else

      # If previous seq is a branch we need to build conditional back transitions, to the end state
      # of each branch (based on the same criteria that originally split the branch)
      if prev_seq.split?
        begin
          #puts "\nDEBUG: *** BUILDING SEQ TO SPLIT #{sequence.inspect} (#{i})"
          build_triggered_back(sequence, prev_seq)
        rescue => x
          puts x.inspect
          puts "Failed in Seq [#{sequence.inspect}] (#{i}) - to create back events to Previous Seq #{prev_seq}"
          raise x
        end

      elsif prev_seq.last
        #puts "\nDEBUG: *** BUILDING SEQ #{sequence.inspect} (#{i})"
        create_back(sequence.first, prev_seq.last)
      end

      # The simple navigation through states within the sequence
      create_pairs sequence

      #puts "\nDEBUG: *** CREATED PAIRS FOR SEQ #{sequence.inspect} (#{i})"
      create_next(sequence.last, next_seq.first) if next_seq.first.present?
    end
  end
end
build_split_sequence_events(sequence, prev_seq, next_seq) click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 121
def build_split_sequence_events(sequence, prev_seq, next_seq)
  # puts "\n\nDEBUG: PROCESS SPLIT SEQ #{sequence.inspect}"
  # puts "DEBUG: SPLIT prev_seq #{prev_seq.inspect}"
  # puts "DEBUG: SPLIT next_seq #{next_seq.inspect}"

  # Create BACK from this entry state to the exit point of any PREVIOUS sequence
  create_back(sequence.split_entry_state, prev_seq.last) if prev_seq.last

  branch_sequence_map.branches_for(sequence).each do |branch|
    begin
      # puts "\n\nDEBUG: Process Branch - #{branch.inspect}"

      # Back and next for any states within the split sequence itself
      create_pairs branch

      # N.B A split sequence can actually be empty
      #
      # i.e Some branches may jump straight from the split point straight to next common sequence

      # Now work out the start and end points for this split.
      next_state = branch.empty? ? next_seq.first : branch.first

      # back from first seq state (or if empty next sequence) to this decision state
      split_entry_state = sequence.split_entry_state

      # If branch has no states, a VALUE triggered BACK will be created later
      create_back(branch.first, split_entry_state) unless branch.empty?

      build_triggered_next(branch, split_entry_state, next_state)

      # N.B When multiple splits occur one after the other, branch.last can equal next_seq.first

      # Not sure if that's reflective that logic not too clever elsewhere but for now
      # make sure we don't create such a next event to itself

      # LAST item in branch connects to FIRST item of NEXT sequence (unless empty and already built with trigger)

      if !branch.empty? && next_seq.first && (branch.last != next_seq.first)
        create_next(branch.last, next_seq.first)
      end

    rescue => x
      puts x.inspect
      puts "Failed in Split Sequnce to process Branch #{branch.inspect}"
      raise x
    end
  end
end
build_triggered_back(sequence, prev_seq) click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 170
def build_triggered_back(sequence, prev_seq)
  #puts "DEBUG: * BUILD triggered Back for #{sequence.inspect}"

  # Create back from FIRST item of THIS sequence to LAST entry of EACH previous BRANCH
  branch_sequence_map.branches_for(prev_seq).each do |branch|
    # Branches can be empty - i.e chain direct to next common sequence
    # in which case back goes to the split sequence state itself (parent of branch)
    to_state = branch.last.nil? ? prev_seq.first : branch.last

    create_back(sequence.first, to_state) do
      lambda do |o|
        unless o && o.respond_to?(branch.trigger_method)
          raise PlannerBlockError, "Cannot Go back - No such method #{branch.trigger_method} on Class #{o.class}"
        end
        o.send(branch.trigger_method) == branch.trigger_value
      end
    end
  end
end
build_triggered_next(branch, from, to) click to toggle source
# File lib/datashift_journey/state_machines/planner.rb, line 190
def build_triggered_next(branch, from, to)
  # N.B sequences can self terminate i.e no further sequences and end of the journey
  return unless from && from != to

  create_next(from, to) do
    lambda do |o|
      unless o && o.respond_to?(branch.trigger_method)
        raise PlannerBlockError, "Cannot split - No such method #{branch.trigger_method} on Class #{o.class}"
      end
      o.send(branch.trigger_method) == branch.trigger_value
    end
  end
end