Skip to main content

chelae_lib/
adapter_db.rs

1//! Built-in 3' adapter sequence presets for common short-read sequencing kits.
2//!
3//! Sources (consulted April 2026):
4//! - Illumina TruSeq — adapter sequence document (rev H); widely used across Illumina WGS,
5//!   WES, ChIP-seq, RNA-seq.
6//! - Illumina Nextera / Tn5 — same sequence on both mates (ligated via transposition).
7//! - Illumina small-RNA / TruSeq Small-RNA — 3' adapter only; single-end by convention.
8//! - Element Biosciences AVITI Elevate — Library Sequences Technical Note LT-00014 (Mar 2024).
9//! - MGI / DNBSEQ / BGI — documented in MGI's oligo primer docs and cross-referenced in
10//!   `OpenGene/fastp` issue #259 and `linsalrob/mgi-adapters`.
11//!
12//! Element Freestyle and Ultima Illumina-compatible libraries typically use the `truseq`
13//! preset; Ultima's native adapters and Singular G4's S1/S2 adapters are not publicly
14//! disclosed and are therefore not included.
15
16/// A named adapter preset. When `seq_r2` is `None` the preset covers R1-only libraries
17/// (historically single-end such as small-RNA-seq) or the R1 adapter is also used on R2.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct KitAdapter {
20    pub name: &'static str,
21    pub seq_r1: &'static [u8],
22    pub seq_r2: Option<&'static [u8]>,
23}
24
25/// Illumina TruSeq (standard and TruSeq LT/HT): used across most Illumina WGS/WES/RNA-seq.
26pub const TRUSEQ: KitAdapter = KitAdapter {
27    name: "truseq",
28    seq_r1: b"AGATCGGAAGAGCACACGTCTGAACTCCAGTCA",
29    seq_r2: Some(b"AGATCGGAAGAGCGTCGTGTAGGGAAAGAGTGT"),
30};
31
32/// Illumina Nextera / Tn5: used for Nextera DNA, ATAC-seq, Nextera XT.
33/// Same adapter ligated on both mates by the transposition reaction.
34pub const NEXTERA: KitAdapter = KitAdapter {
35    name: "nextera",
36    seq_r1: b"CTGTCTCTTATACACATCT",
37    seq_r2: Some(b"CTGTCTCTTATACACATCT"),
38};
39
40/// Illumina TruSeq Small-RNA / miRNA — 3' adapter. Single-end by convention;
41/// no canonical R2 adapter.
42pub const SMALL_RNA: KitAdapter =
43    KitAdapter { name: "small-rna", seq_r1: b"TGGAATTCTCGGGTGCCAAGG", seq_r2: None };
44
45/// Element Biosciences AVITI "Elevate" (native) adapters.
46/// AVITI Adept / Cloudbreak Freestyle libraries use standard Illumina (TruSeq) adapters
47/// and should therefore use the `truseq` preset instead of this one.
48pub const AVITI: KitAdapter = KitAdapter {
49    name: "aviti",
50    seq_r1: b"ATGTCGGAAGGTGTGCAGGCTACCGCTTGTCAACT",
51    seq_r2: Some(b"ATGTCGGAAGGTGTCTGGTGAGCCAATC"),
52};
53
54/// MGI / DNBSEQ / BGI native adapters. Note that many MGI library preps sold in the
55/// US/EU actually use Illumina-style adapters — `truseq` may be the correct preset
56/// for such libraries despite the instrument.
57pub const MGI: KitAdapter = KitAdapter {
58    name: "mgi",
59    seq_r1: b"AAGTCGGAGGCCAAGCGGTCTTAGGAAGACAA",
60    seq_r2: Some(b"AAGTCGGATCGTAGCCATGTCGTTCTGTGAGCCAAGGAGTTG"),
61};
62
63/// Every published kit preset, in stable order.
64pub const ALL_KITS: &[KitAdapter] = &[TRUSEQ, NEXTERA, SMALL_RNA, AVITI, MGI];
65
66/// Returns the named kit or `None` if `name` is not recognized. The special alias
67/// `"all"` is not a single kit and returns `None` here; callers wanting to union every
68/// kit should use [`expand_kit_name`] instead.
69pub fn kit_by_name(name: &str) -> Option<&'static KitAdapter> {
70    ALL_KITS.iter().find(|k| k.name.eq_ignore_ascii_case(name))
71}
72
73/// Expands a single user-supplied kit spelling into zero or more `KitAdapter` slices.
74/// Recognizes kit names and the special `"all"` alias.
75pub fn expand_kit_name(name: &str) -> Option<&'static [KitAdapter]> {
76    if name.eq_ignore_ascii_case("all") {
77        return Some(ALL_KITS);
78    }
79    if name.eq_ignore_ascii_case("dnbseq") {
80        return Some(std::slice::from_ref(&MGI));
81    }
82    kit_by_name(name).map(std::slice::from_ref)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn kit_by_name_known() {
91        assert_eq!(kit_by_name("truseq"), Some(&TRUSEQ));
92        assert_eq!(kit_by_name("TRUSEQ"), Some(&TRUSEQ));
93        assert_eq!(kit_by_name("nextera"), Some(&NEXTERA));
94        assert_eq!(kit_by_name("small-rna"), Some(&SMALL_RNA));
95        assert_eq!(kit_by_name("aviti"), Some(&AVITI));
96        assert_eq!(kit_by_name("mgi"), Some(&MGI));
97    }
98
99    #[test]
100    fn kit_by_name_unknown() {
101        assert_eq!(kit_by_name("notakit"), None);
102        // "all" is a collective alias; not a single kit.
103        assert_eq!(kit_by_name("all"), None);
104    }
105
106    #[test]
107    fn expand_kit_name_all_unions_every_kit() {
108        let expanded = expand_kit_name("all").unwrap();
109        assert_eq!(expanded.len(), ALL_KITS.len());
110    }
111
112    #[test]
113    fn expand_kit_name_dnbseq_alias() {
114        let expanded = expand_kit_name("dnbseq").unwrap();
115        assert_eq!(expanded, std::slice::from_ref(&MGI));
116    }
117
118    #[test]
119    fn expand_kit_name_unknown_returns_none() {
120        assert_eq!(expand_kit_name("mystery"), None);
121    }
122
123    #[test]
124    fn adapters_are_uppercase_acgt_only() {
125        for kit in ALL_KITS {
126            for seq in [Some(kit.seq_r1), kit.seq_r2].into_iter().flatten() {
127                for &b in seq {
128                    assert!(
129                        matches!(b, b'A' | b'C' | b'G' | b'T'),
130                        "kit {} has non-ACGT byte {:?} in adapter",
131                        kit.name,
132                        b as char,
133                    );
134                }
135            }
136        }
137    }
138}