sanger/sequencescape

View on GitHub
app/models/submission/linear_request_graph.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
98%
# frozen_string_literal: true
# This module can be included where the {Order} has a linear behaviour,
# with no branching. Eg. in {LinearSubmission}
module Submission::LinearRequestGraph
  # Source data is used to pass information down the request graph
  # asset
  # @attr asset             [Receptacle, nil]   The asset from which the request will be build.
  #                                              nil indicates no upstream asset in cases where target assets
  #                                              are generated later.
  # @attr qc_metric         [QcMetric]     The Qc Metric associated with this asset for this request type
  # @attr previous_requests [Array<Request>, nil] Used to pass requests down the chain when building the
  #                                        request graph. Used to. eg. pass down libraries
  SourceData = Struct.new(:asset, :qc_metric, :sample)

  # Builds the entire request graph for this {Order}
  # This is called from {Submission#process_submission!} which processes each order in turn,
  # multiplexing_assets returned by the first order get passed into subsequent orders.
  # @note The block behaviour here looks a bit odd, and is a result of the previous behaviour
  #       in which the multiplexing assets were yielded directly to the submission.
  #       This behaviour can be simplified eventually, but is maintained for the time being to
  #       reduce risk of a more significant re-write.
  def build_request_graph!(multiplexing_assets = nil)
    ActiveRecord::Base.transaction do
      mx_assets_tmp = nil
      create_request_chain!(
        build_request_type_multiplier_pairs,
        assets.map { |asset| SourceData.new(asset, asset.latest_stock_metrics(product), nil) },
        multiplexing_assets
      ) { |a| mx_assets_tmp = a }
      mx_assets_tmp
    end
  end

  private

  # Returns an array of arrays.
  # The inner array has two elements: a RequestType instance, and an integer (the "multiplier").
  # e.g. [ [ RequestType instance 1, 1 ], [ RequestType instance 2, 1 ] ]
  def build_request_type_multiplier_pairs # rubocop:todo Metrics/AbcSize
    # Ensure that the keys of the multipliers hash are strings, otherwise we get weirdness!
    multipliers =
      Hash
        .new { |h, k| h[k] = 1 }
        .tap do |multipliers|
          requested_multipliers = request_options.try(:[], :multiplier) || {}
          requested_multipliers.each { |k, v| multipliers[k.to_s] = v.to_i }
        end

    request_types.dup.map { |request_type_id| [RequestType.find(request_type_id), multipliers[request_type_id.to_s]] }
  end

  def create_target_asset_for!(request_type, source_asset = nil)
    request_type.create_target_asset! do |asset|
      asset.generate_barcode
      asset.generate_name(source_asset&.name || asset.try(:human_barcode))
    end
  end

  # Creates the next step in the request graph, taking the first request type specified and building
  # enough requests for the source assets.  It will recursively call itself if there are more requests
  # that need creating.
  # @yieldreturn [Array<Asset>] For orders with multiplexed request types, yields the target asset of
  #                             the multiplexing, such as a {MultiplexedLibraryTube}.
  # rubocop:todo Metrics/PerceivedComplexity, Metrics/MethodLength, Metrics/AbcSize
  def create_request_chain!(request_type_and_multiplier_pairs, source_data_set, multiplexing_assets, &block) # rubocop:todo Metrics/CyclomaticComplexity
    raise StandardError, 'No request types specified!' if request_type_and_multiplier_pairs.empty?

    request_type, multiplier = request_type_and_multiplier_pairs.shift

    # rubocop:todo Metrics/BlockLength
    multiplier.times do
      # If the request type is for multiplexing it means that all of the assets end up in one target asset.
      # Otherwise there are the same number of target assets as source.
      target_assets =
        if request_type.for_multiplexing?
          multiplexing_assets || Array.new(source_data_set.length, create_target_asset_for!(request_type))
        else
          source_data_set.map { |source_data| create_target_asset_for!(request_type, source_data.asset) }
        end
      yield(target_assets) if block && request_type.for_multiplexing?

      # Now we can iterate over the source assets and target assets building the requests between them.
      # Ensure that the request has the correct comments on it, and that the aliquots of the source asset
      # are transferred into the destination if the request does not do this in some manner itself.
      source_data_set.each_with_index do |source_data, index|
        source_asset = source_data.asset
        qc_metrics = source_data.qc_metric
        target_asset = target_assets[index]

        create_request_of_type!(request_type, asset: source_asset, target_asset: target_asset).tap do |request|
          # TODO: AssetLink is supposed to disappear at some point in the future because it makes no real sense
          # given that the request graph describes this relationship.
          # JG: Its removal only really makes sense if we can walk the request graph in a timely manner.
          # We use save not save! as AssetLink throws validation errors when the link already exists
          if source_asset&.labware.present? && target_asset&.labware.present?
            AssetLink.create_edge(source_asset.labware, target_asset.labware)
          end

          request.qc_metrics = qc_metrics.compact.uniq
          request.update_responsibilities!

          if comments.present?
            comments.split("\n").each { |comment| request.comments.create!(user: user, description: comment) }
          end
        end
      end

      # Now we can continue to the next request type in the chain, using the target assets we've created.
      # We need to de-duplicate the multiplexed assets.  Note that we duplicate the pairs here so that
      # they don't get disrupted by the shift operation at the start of this method.
      next if request_type_and_multiplier_pairs.empty?

      target_data_set =
        if request_type.for_multiplexing?
          # May have many nil assets for non-multiplexing
          if multiplexing_assets.nil?
            criteria = source_data_set.map(&:qc_metric).flatten.uniq
            target_assets.uniq.map { |asset| SourceData.new(asset, criteria, nil) }
          else
            associate_built_requests(target_assets.uniq.compact)
            []
          end
        else
          target_assets.each_with_index.map do |asset, index|
            source_asset = request_type.no_target_asset? ? source_data_set[index].first : asset
            SourceData.new(source_asset, source_data_set[index].qc_metric, nil)
          end
        end

      create_request_chain!(request_type_and_multiplier_pairs.dup, target_data_set, multiplexing_assets, &block)
    end
    # rubocop:enable Metrics/BlockLength
  end

  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity

  def associate_built_requests(assets) # rubocop:todo Metrics/AbcSize
    assets
      .map(&:requests)
      .flatten
      .each do |request|
        request.update!(initial_study: nil) if request.initial_study != study
        request.update!(initial_project: nil) if request.initial_project != project
        if comments.present?
          comments.split("\n").each { |comment| request.comments.create!(user: user, description: comment) }
        end
      end
  end
end