use serde::{Deserialize, Serialize}; use crate::{plate::PlateFormat, transfer::Transfer, util::num_to_letters}; /// Represents a single line of a CSV picklist. /// In practice, this is generated from the deserialize intermediate. /// A list of `TransferRecord`s can be used to create a `transfer::Transfer` struct; /// see the `auto` module for this. #[derive(Serialize, Debug, Clone)] pub struct TransferRecord { #[serde(rename = "Source Barcode")] pub source_plate: String, #[serde(rename = "Source Well")] pub source_well: String, #[serde(rename = "Dest Barcode")] pub destination_plate: String, #[serde(rename = "Destination Well")] pub destination_well: String, #[serde(rename = "Transfer Volume")] pub volume: f32, #[serde(rename = "Concentration")] pub concentration: Option, } /// The deserialization intermediate is generated from a mangled CSV s.t. /// the column headers are standardized to those given to the derive macro. /// /// `TransferRecord` can *always* be generated from the intermediate. /// Currently there's no reason why the inverse couldn't be true, /// but there's no reason to convert back into the deserialize-only intermediate. #[derive(Deserialize, Debug, Clone)] pub struct TransferRecordDeserializeIntermediate { #[serde(rename = "sourceplate")] source_plate: String, #[serde(rename = "destinationplate")] destination_plate: String, #[serde(rename = "sourcewell")] source_well: String, #[serde(rename = "sourceformat")] source_format: Option, #[serde(rename = "destinationwell")] destination_well: String, #[serde(rename = "destinationformat")] destination_format: Option, #[serde(rename = "volume")] volume: Option, #[serde(rename = "concentration")] concentration: Option, } impl From for TransferRecord { fn from(value: TransferRecordDeserializeIntermediate) -> Self { let mut source_well: String = value.source_well; if let Some(pformat) = value .source_format .and_then(|x| PlateFormat::try_from(x.as_str()).ok()) { if let Ok(well_number) = source_well.parse::() { if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) { source_well = alphanumeric; } } } let mut destination_well: String = value.destination_well; if let Some(pformat) = value .destination_format .and_then(|x| PlateFormat::try_from(x.as_str()).ok()) { if let Ok(well_number) = destination_well.parse::() { if let Some(alphanumeric) = numeric_well_to_alphanumeric(well_number, pformat) { destination_well = alphanumeric; } } } let volume = value.volume.unwrap_or(2.5f32); TransferRecord { source_plate: value.source_plate, destination_plate: value.destination_plate, source_well, destination_well, volume, concentration: value.concentration, } } } impl TransferRecordDeserializeIntermediate { /// Used to cull malformed picklist entries that lack required information pub fn is_empty(&self) -> bool { self.source_plate.is_empty() || self.destination_plate.is_empty() || self.source_well.is_empty() || self.destination_well.is_empty() } } fn numeric_well_to_alphanumeric(input: u16, pformat: PlateFormat) -> Option { let column_height: u16 = pformat.size().0 as u16; let column = input.div_ceil(column_height); let row = input % column_height; let row_str = num_to_letters(row as u8)?; Some(format!("{}{}", row_str, column)) }