sanger/sequencescape

View on GitHub
app/models/barcode.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
93%
# frozen_string_literal: true

# A barcode is an identifier for a piece of labware which is attached via a
# printed label. Barcodes may either be generated by Sequencescape, or may get
# supplied externally. In some cases labware may have more than one barcode assigned.
#
# @author [grl]
#
class Barcode < ApplicationRecord
  require 'sanger_barcode_format'
  require 'sanger_barcode_format/legacy_methods'
  extend SBCF::LegacyMethods

  # This association will have to be removed in a subsequent story
  belongs_to :asset, optional: false, class_name: 'Labware'
  # New association introduced in #4121. This is a temporary measure to allow the renaming
  # of the asset association to labware. This was created to accommodate for_search_query scope's include() method
  # belongs_to :labware, class_name: 'Labware', optional: false
  before_validation :serialize_barcode

  # See #4121 - renaming asset terminology to labware
  # See #4121 - The actual table column will be renamed in a subsequent story
  alias_association :labware, :asset
  alias_attribute :labware_id, :asset_id

  after_commit :broadcast_barcode

  # Caution! Do not adjust the index of existing formats.
  enum format: {
         sanger_ean13: 0,
         infinium: 1,
         fluidigm: 2,
         external: 3,
         # gone. Don't do this.
         aker_barcode: 4,
         cgap: 5,
         sanger_code39: 6,
         fluidx_barcode: 7,
         uk_biocentre_v1: 8,
         uk_biocentre_v2: 9,
         uk_biocentre_unid: 10,
         alderly_park_v1: 11,
         alderly_park_v2: 12,
         uk_biocentre_v3: 13,
         cgap_plate: 14,
         cgap_rack: 15,
         glasgow: 16,
         cambridge_a_z: 17,
         heron_tailed: 18,
         randox: 19,
         uk_biocentre_v4: 20,
         cambridge_a_z_v2: 21,
         glasgow_v2: 22,
         eagle: 23,
         cambridge_a_z_eagle: 24,
         glasgow_eagle: 25,
         uk_biocentre_eagle: 26,
         alderley_park_eagle: 27,
         randox_eagle: 28,
         randox_v2: 29,
         glasgow_v3: 30,
         uk_biocentre_v5: 31,
         health_services_laboratories_v1: 32,
         uk_biocentre_v6: 33,
         brants_bridge: 34,
         leamington_spa: 35,
         newcastle: 36,
         brants_bridge_v2: 37,
         uk_biocentre_v7: 38,
         east_london_genes_and_health: 39,
         leamington_spa_v2: 40,
         east_london_genes_and_health_v2: 41,
         sequencescape22: 42,
         plymouth_v2: 43,
         leamington_spa_v3: 44,
         brants_bridge_v3: 45,
         ibd_response: 46,
         rvi: 47
       }

  # Barcode formats which may be submitted via sample manifests
  FOREIGN_BARCODE_FORMATS = %i[
    cgap
    fluidx_barcode
    fluidigm
    uk_biocentre_v1
    uk_biocentre_v2
    uk_biocentre_unid
    alderly_park_v1
    alderly_park_v2
    uk_biocentre_v3
    cgap_plate
    cgap_rack
    glasgow
    cambridge_a_z
    heron_tailed
    randox
    uk_biocentre_v4
    cambridge_a_z_v2
    glasgow_v2
    eagle
    cambridge_a_z_eagle
    glasgow_eagle
    uk_biocentre_eagle
    alderley_park_eagle
    randox_eagle
    randox_v2
    glasgow_v3
    uk_biocentre_v5
    health_services_laboratories_v1
    uk_biocentre_v6
    brants_bridge
    leamington_spa
    newcastle
    brants_bridge_v2
    uk_biocentre_v7
    east_london_genes_and_health
    leamington_spa_v2
    east_london_genes_and_health_v2
    sequencescape22
    plymouth_v2
    leamington_spa_v3
    brants_bridge_v3
    ibd_response
    rvi
  ].freeze

  validate :barcode_valid?
  validates :barcode, uniqueness: { scope: :format, case_sensitive: false }
  scope(
    :sanger_barcode,
    lambda do |prefix, number|
      human_barcode = SBCF::SangerBarcode.from_prefix_and_number(prefix, number).human_barcode
      where(format: %i[sanger_ean13 sanger_code39], barcode: human_barcode)
    end
  )
  scope :for_search_query, ->(*input) { where(barcode: Barcode.extract_barcodes(input)).includes(:asset) }

  delegate :=~, to: :handler
  delegate_missing_to :handler

  def self.build_sanger_ean13(attributes)
    build_sanger_barcode(attributes, format: :sanger_ean13)
  end

  def self.build_sanger_code39(attributes)
    build_sanger_barcode(attributes, format: :sanger_code39)
  end

  def self.build_sequencescape22(attributes)
    new(format: :sequencescape22, barcode: attributes[:barcode])
  end

  def self.build_sanger_barcode(attributes, format:)
    # We need to symbolize our hash keys to allow them to get passed in to named arguments.
    safe_attributes = attributes.slice(:number, :prefix, :human_barcode, :machine_barcode).symbolize_keys
    new(format: format, barcode: SBCF::SangerBarcode.new(**safe_attributes).human_barcode)
  end

  # Extract barcode from user input
  def self.extract_barcode(barcode)
    [barcode.to_s].tap { |barcodes| barcodes << SBCF::SangerBarcode.from_user_input(barcode.to_s).human_barcode }
      .compact
      .uniq
  end

  # Returns the barcode format matching the supplied barcode
  def self.matching_barcode_format(possible_barcode)
    FOREIGN_BARCODE_FORMATS.detect do |cur_format|
      bc = Barcode.new(format: cur_format, barcode: possible_barcode)
      bc.handler.valid?
    end
  end

  def self.exists_for_format?(barcode_format, search_barcode)
    Barcode.exists?(format: barcode_format, barcode: search_barcode)
  end

  def self.extract_barcodes(barcodes)
    barcodes
      .flatten
      .each_with_object([]) do |source_bc, store|
        next if source_bc.blank?

        store.concat(Barcode.extract_barcode(source_bc))
      end
  end

  def sequencescape22?
    format == 'sequencescape22'
  end

  def handler
    @handler ||= handler_class.new(barcode)
  end

  def handler_class_name
    format.classify
  end

  # If the barcode changes, we'll need a new handler. This allows handlers themselves to be immutable.
  def barcode=(new_barcode)
    @handler = nil
    super
  end

  def sanger_barcode?
    sanger_ean13? || sanger_code39?
  end

  def child_barcodes
    Barcode.where('barcode LIKE ?', "#{barcode}-%")
  end

  private

  def barcode_valid?
    errors.add(:barcode, "is not an acceptable #{format} barcode") unless handler.valid?
  end

  def handler_class
    Barcode::FormatHandlers.const_get(handler_class_name, false)
  end

  def broadcast_barcode
    Messenger.new(template: 'BarcodeIO', root: 'barcode', target: self).broadcast
  end
end