use clap::ValueEnum;
use super::catalogs;
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum AdapterPreset {
None,
#[value(name = "illumina-truseq", alias = "illumina-tru-seq")]
IlluminaTruSeq,
#[value(name = "illumina-nextera", alias = "illumina-tagmentation")]
IlluminaNextera,
#[value(name = "illumina-truseq-small-rna", alias = "illumina-small-rna")]
IlluminaTruSeqSmallRna,
#[value(name = "illumina-stranded-rna-ligation")]
IlluminaStrandedRnaLigation,
#[value(name = "illumina-scriptseq-truseq-methylation")]
IlluminaScriptSeqTruSeqMethylation,
#[value(name = "mgi-dnbseq", alias = "mgi-bgi", alias = "bgi-dnbseq")]
MgiDnbSeq,
#[value(name = "qiaseq-mirna", alias = "qiaseq")]
QiaSeqMirna,
}
impl AdapterPreset {
pub(crate) const fn catalog(self) -> Option<&'static AdapterCatalog> {
match self {
Self::None => None,
Self::IlluminaTruSeq => Some(&catalogs::illumina::ILLUMINA_TRUSEQ),
Self::IlluminaNextera => Some(&catalogs::illumina::ILLUMINA_NEXTERA),
Self::IlluminaTruSeqSmallRna => Some(&catalogs::illumina::ILLUMINA_TRUSEQ_SMALL_RNA),
Self::IlluminaStrandedRnaLigation => {
Some(&catalogs::illumina::ILLUMINA_STRANDED_RNA_LIGATION)
}
Self::IlluminaScriptSeqTruSeqMethylation => {
Some(&catalogs::illumina::ILLUMINA_SCRIPTSEQ_TRUSEQ_METHYLATION)
}
Self::MgiDnbSeq => Some(&catalogs::mgi::MGI_DNBSEQ),
Self::QiaSeqMirna => Some(&catalogs::qiaseq::QIASEQ_MIRNA),
}
}
#[cfg(test)]
const CATALOG_PRESETS: &'static [Self] = &[
Self::IlluminaTruSeq,
Self::IlluminaNextera,
Self::IlluminaTruSeqSmallRna,
Self::IlluminaStrandedRnaLigation,
Self::IlluminaScriptSeqTruSeqMethylation,
Self::MgiDnbSeq,
Self::QiaSeqMirna,
];
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct AdapterCatalog {
pub(crate) id: &'static str,
pub(crate) name: &'static str,
pub(crate) entries: &'static [AdapterEntry],
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct DnaSequence(&'static [u8]);
impl DnaSequence {
pub(crate) const fn from_iupac_ascii(sequence: &'static [u8]) -> Self {
assert!(
!sequence.is_empty(),
"catalog DNA sequence must not be empty"
);
let mut index = 0;
while index < sequence.len() {
match sequence[index] {
b'A' | b'C' | b'G' | b'T' | b'U' | b'R' | b'Y' | b'S' | b'W' | b'K' | b'M'
| b'B' | b'D' | b'H' | b'V' | b'N' => {}
_ => panic!("catalog DNA sequence must use uppercase IUPAC bases"),
}
index += 1;
}
Self(sequence)
}
pub(crate) const fn as_bytes(self) -> &'static [u8] {
self.0
}
}
impl AdapterCatalog {
pub(crate) fn current_suffix_trim_entries(
&self,
) -> impl Iterator<Item = &'static AdapterEntry> {
self.entries.iter().filter(|entry| {
entry.role == SequenceRole::Adapter && entry.geometry == MatchGeometry::ThreePrimeSuffix
})
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct AdapterEntry {
pub(crate) id: &'static str,
pub(crate) name: &'static str,
pub(crate) sequence: DnaSequence,
pub(crate) role: SequenceRole,
pub(crate) platform: SequencingPlatform,
pub(crate) kit: &'static KitRef,
pub(crate) geometry: MatchGeometry,
pub(crate) source: &'static SourceRef,
}
#[allow(dead_code)]
impl AdapterEntry {
pub(crate) const fn three_prime_adapter(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Adapter,
platform,
kit,
MatchGeometry::ThreePrimeSuffix,
source,
)
}
pub(crate) const fn both_end_adapter(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Adapter,
platform,
kit,
MatchGeometry::BothEnds,
source,
)
}
pub(crate) const fn catalog_adapter(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Adapter,
platform,
kit,
MatchGeometry::CatalogOnly,
source,
)
}
pub(crate) const fn internal_adapter(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Adapter,
platform,
kit,
MatchGeometry::InternalMarker,
source,
)
}
pub(crate) const fn five_prime_primer(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Primer,
platform,
kit,
MatchGeometry::FivePrimePrefix,
source,
)
}
pub(crate) const fn three_prime_primer(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Primer,
platform,
kit,
MatchGeometry::ThreePrimeSuffix,
source,
)
}
pub(crate) const fn catalog_barcode(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Barcode,
platform,
kit,
MatchGeometry::CatalogOnly,
source,
)
}
pub(crate) const fn catalog_flank(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
platform: SequencingPlatform,
kit: &'static KitRef,
source: &'static SourceRef,
) -> Self {
Self::new(
id,
name,
sequence,
SequenceRole::Flank,
platform,
kit,
MatchGeometry::CatalogOnly,
source,
)
}
#[expect(
clippy::too_many_arguments,
reason = "private funnel keeps role-specific catalog constructors consistent"
)]
const fn new(
id: &'static str,
name: &'static str,
sequence: DnaSequence,
role: SequenceRole,
platform: SequencingPlatform,
kit: &'static KitRef,
geometry: MatchGeometry,
source: &'static SourceRef,
) -> Self {
Self {
id,
name,
sequence,
role,
platform,
kit,
geometry,
source,
}
}
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SequenceRole {
Adapter,
Primer,
Barcode,
Flank,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SequencingPlatform {
Illumina,
MgiBgi,
QiaSeq,
PacBio,
OxfordNanopore,
}
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MatchGeometry {
ThreePrimeSuffix,
FivePrimePrefix,
BothEnds,
InternalMarker,
CatalogOnly,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct KitRef {
pub(crate) vendor: &'static str,
pub(crate) name: &'static str,
pub(crate) version: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SourceRef {
pub(crate) label: &'static str,
pub(crate) url: Option<&'static str>,
pub(crate) version: Option<&'static str>,
pub(crate) note: Option<&'static str>,
}
#[cfg(test)]
mod tests {
use super::{AdapterPreset, DnaSequence, MatchGeometry, SequenceRole, catalogs};
#[test]
fn all_non_none_presets_have_catalog_entries() {
for preset in AdapterPreset::CATALOG_PRESETS {
let catalog = preset.catalog().expect("preset should have a catalog");
assert!(
!catalog.entries.is_empty(),
"{} should not be empty",
catalog.id
);
}
}
#[test]
fn none_preset_has_no_catalog() {
assert!(AdapterPreset::None.catalog().is_none());
}
#[test]
fn dna_sequence_accepts_uppercase_iupac_bases() {
assert_eq!(
DnaSequence::from_iupac_ascii(b"ACGTURYSWKMBDHVN").as_bytes(),
b"ACGTURYSWKMBDHVN"
);
}
#[test]
#[should_panic(expected = "catalog DNA sequence must use uppercase IUPAC bases")]
fn dna_sequence_rejects_non_iupac_bases() {
DnaSequence::from_iupac_ascii(b"ACGTX");
}
#[test]
fn current_suffix_trim_entries_are_only_supported_adapters() {
let catalog = &catalogs::ont::ONT_NATIVE_BARCODING_V14;
for entry in catalog.current_suffix_trim_entries() {
assert_eq!(entry.role, SequenceRole::Adapter);
assert_eq!(entry.geometry, MatchGeometry::ThreePrimeSuffix);
}
}
#[test]
fn ont_native_barcodes_are_not_current_suffix_trim_targets() {
let catalog = &catalogs::ont::ONT_NATIVE_BARCODING_V14;
assert!(
catalog
.entries
.iter()
.any(|entry| entry.role == SequenceRole::Barcode)
);
assert!(
catalog
.current_suffix_trim_entries()
.all(|entry| entry.role != SequenceRole::Barcode)
);
}
}