sanger/limber

View on GitHub
app/models/robots/robot.rb

Summary

Maintainability
A
3 hrs
Test Coverage
A
100%
# frozen_string_literal: true

module Robots
  # Core robot class. Used when plates have a simple
  # 1:1 parent child relationship.
  # Todo: Improve class length by using rails error handling
  # rubocop:disable Metrics/ClassLength
  class Robot
    include Form

    attr_reader :beds
    attr_accessor :api, :user_uuid, :layout, :name, :id, :verify_robot, :class, :robot_barcode, :require_robot

    alias verify_robot? verify_robot
    alias require_robot? require_robot

    def well_order
      :coordinate
    end

    def perform_transfer(bed_settings)
      beds.each do |id, bed|
        bed.load(bed_settings[id]) if bed.transitions?
        bed.valid? || raise(Bed::BedError, bed.error_messages)
      end
      beds.each_value(&:transition)
    end

    def formatted_message
      error_messages.join(' ')
    end

    def verify(params)
      assign_attributes(params)
      validation_report
    end

    def validation_report
      verified = valid_labwares.merge(valid_relationships) { |_k, v1, v2| v1 && v2 }
      verified['robot'] = valid_robot
      Report.new(verified, verified.values.all?, formatted_message)
    end

    def beds=(new_beds)
      beds = Hash.new { |store, barcode| store[barcode] = Robots::Bed::Invalid.new(barcode) }
      new_beds.each { |id, bed| beds[id] = bed_class.new(bed.merge(robot: self)) }
      @beds = beds
    end

    def bed_labwares=(bed_labwares)
      bed_labwares.each { |bed_barcode, labware_barcodes| beds[bed_barcode.strip].load(labware_barcodes) }
    end

    private

    def error(bed, message)
      error_messages << "#{bed.label}: #{message}"
      false
    end

    def bed_error(bed)
      error_messages << bed.formatted_message
      false
    end

    def error_messages
      @error_messages ||= []
    end

    def valid_robot
      return false unless robot_present_if_required

      return true unless verify_robot? && beds.values.first.labware.present?

      if missing_custom_metadatum_collection || original_robot != robot_barcode
        error_messages << 'Your labware is not on the right robot'
        return false
      end
      true
    end

    def robot_present_if_required
      if require_robot? && robot_barcode.blank?
        error_messages << 'Please scan the robot barcode'
        return false
      end
      true
    end

    def missing_custom_metadatum_collection
      beds.values.first.labware.custom_metadatum_collection.nil?
    end

    def original_robot
      return nil if missing_custom_metadatum_collection

      beds.values.first.labware.custom_metadatum_collection.metadata['created_with_robot']
    end

    def valid_labwares
      beds.transform_values { |bed| bed.valid? || bed_error(bed) }
    end

    #
    # Returns a hash of bed barcodes and their valid state
    # Also adds any errors describing invalid bed states
    #
    # @return [Hash< String => Boolean>] Hash of boolean indexed by bed barcode
    def valid_relationships
      parents_and_position { |parents, position| check_labware_identity(parents, position) }.compact
    end

    def bed_class
      Robots::Bed::Base
    end

    def parents_and_position
      recognised_beds.transform_values do |bed|
        next if bed.parents.blank?

        bed.parents.all? { |parent_bed_barcode| yield(bed.parent_labware, parent_bed_barcode) }
      end
    end

    # Check whether the labware scanned onto the indicated bed
    # matches the expected labwares. Records any errors.
    #
    # @param parents [Array] An array of parent labwares
    # @param position [String] The barcode of the bed expected to contain the labwares
    # @return [Boolean] True if valid, false otherwise
    def check_labware_identity(parents, position)
      if parents.empty?
        check_labware_identity_when_not_expecting_a_labware(position)
      else
        check_labware_identity_when_expecting_a_labware(parents, position)
      end
    end

    # Check whether the indicated bed is valid when we are not expecting anything.
    # Records any errors.
    #
    # @param position [String] The barcode of the bed
    # @return [Boolean] True if valid, false otherwise
    def check_labware_identity_when_not_expecting_a_labware(position)
      # We have not scanned a labware, and no scanned labwares are expected (valid)
      return true if beds[position].labware.nil?

      # We have a shared parent and the shared parent contains a labware, but one of the target beds
      # does not e.g. PhiX tube shared on a robot with multiple transfers (valid)
      return true if beds[position].shared_parent

      # We have scanned a labware, but weren't expecting one (invalid)
      msg =
        # rubocop:todo Layout/LineLength
        'Either the labware scanned into this bed should not be here, or the related labware(s) have not been scanned into their beds.'

      # rubocop:enable Layout/LineLength
      error(beds[position], msg)
      false
    end

    # Generate the appropriate error message for this bed and scenario
    #
    # @param expected labwares [Array] An array of expected labwares for this bed
    # @param position [String] The barcode of the bed
    # @return [String] The error message
    def generate_error_message(expected_labwares, position)
      if expected_labwares.empty?
        # We were unable to recognize any parent of this plate that matches the
        # labware purpose of the beds in the configuration, so this labware has
        # unexpected parents for this pipeline bed verification
        # rubocop:todo Layout/LineLength
        "Was expected to contain a labware of purpose #{beds[position].purpose} but the scanned child labware does not have a parent with that purpose."
        # rubocop:enable Layout/LineLength
      elsif beds[position].labware.nil?
        # We expected a labware but none was scanned
        # rubocop:todo Layout/LineLength
        "Was expected to contain labware barcode #{expected_labwares.map(&:human_barcode).join(',')} but nothing was scanned (empty)."
        # rubocop:enable Layout/LineLength
      else
        # We have scanned an unexpected labware
        # rubocop:todo Layout/LineLength
        "Was expected to contain labware barcode #{expected_labwares.map(&:human_barcode).join(',')} but contains a different labware."
        # rubocop:enable Layout/LineLength
      end
    end

    # Check whether the indicated bed is valid when we are expecting a specific labware.
    # Records any errors.
    #
    # @param parents [Array] An array of parent labwares
    # @param position [String] The barcode of the bed
    # @return [Boolean] True if valid, false otherwise
    def check_labware_identity_when_expecting_a_labware(parents, position)
      expected_uuids = parents.map(&:uuid)

      # We have scanned a labware, and it is in the list of expected labwares (valid)
      return true if expected_uuids.include?(beds[position].labware.try(:uuid))

      # We have an invalid result
      # Filter the list of parents to expected bed labware purpose at this
      # position, e.g. position takes purpose A, filter parents for purpose A
      expected_labwares = parents.filter { |parent| parent.purpose.name == beds[position].purpose }

      # Identify what looks wrong and create a suitable message
      msg = generate_error_message(expected_labwares, position)
      error(beds[position], msg)
      false
    end

    def recognised_beds
      beds.select { |_barcode, bed| bed.recognised? }
    end
  end
  # rubocop:enable Metrics/ClassLength
end