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}