fastqc_rust/modules/mod.rs
1pub mod adapter_content;
2pub mod basic_stats;
3pub mod duplication_level;
4pub mod gc_content;
5pub mod kmer_content;
6pub mod n_content;
7pub mod overrepresented_seqs;
8pub mod per_base_quality;
9pub mod per_base_sequence_content;
10pub mod per_sequence_quality;
11pub mod per_tile_quality;
12pub mod sequence_length_distribution;
13
14use std::fmt;
15use std::io;
16use std::sync::{Arc, Mutex};
17
18use crate::config::{FastQCConfig, Limits, LimitsExt};
19use crate::sequence::Sequence;
20
21/// Status of a QC module after analysis, matching Java FastQC's pass/warn/fail icons.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ModuleStatus {
24 Pass,
25 Warn,
26 Fail,
27}
28
29impl fmt::Display for ModuleStatus {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 // These exact strings appear in the text report summary section
32 match self {
33 ModuleStatus::Pass => write!(f, "PASS"),
34 ModuleStatus::Warn => write!(f, "WARN"),
35 ModuleStatus::Fail => write!(f, "FAIL"),
36 }
37 }
38}
39
40/// Trait that all QC analysis modules must implement.
41///
42/// Mirrors the `QCModule` Java interface method-for-method
43/// (minus Swing GUI methods like `getResultsPanel()`).
44pub trait QCModule: Send {
45 /// Process a single sequence record, accumulating statistics.
46 fn process_sequence(&mut self, sequence: &Sequence);
47
48 /// The display name of this module as shown in the report.
49 fn name(&self) -> &str;
50
51 /// A longer description of what this module checks.
52 fn description(&self) -> &str;
53
54 /// Reset the module to its initial state.
55 fn reset(&mut self);
56
57 /// Set the source filename for this module.
58 /// BasicStats uses this to display the filename in the report.
59 /// Other modules ignore it.
60 fn set_filename(&mut self, _name: &str) {}
61
62 /// Finalize calculations after all sequences have been processed.
63 ///
64 /// In Java, modules lazily compute results in synchronized
65 /// methods called from getResultsPanel()/makeReport()/raisesError()/raisesWarning().
66 /// In Rust, we separate the mutable computation phase (finalize) from the
67 /// immutable reporting phase so that raises_error/raises_warning/write_text_report
68 /// can take &self.
69 fn finalize(&mut self) {}
70
71 /// Whether the module results should trigger a failure status.
72 fn raises_error(&self) -> bool;
73
74 /// Whether the module results should trigger a warning status.
75 fn raises_warning(&self) -> bool;
76
77 /// Whether this module should skip sequences flagged as filtered.
78 fn ignore_filtered_sequences(&self) -> bool;
79
80 /// Whether this module should be excluded from the report.
81 /// Matches `ignoreInReport()` - used e.g. by PerTileQuality when
82 /// there are no tile IDs in the data.
83 fn ignore_in_report(&self) -> bool;
84
85 /// The current status of this module.
86 fn status(&self) -> ModuleStatus {
87 if self.raises_error() {
88 ModuleStatus::Fail
89 } else if self.raises_warning() {
90 ModuleStatus::Warn
91 } else {
92 ModuleStatus::Pass
93 }
94 }
95
96 /// Return the base filename for this module's chart image (without extension).
97 /// Matches the filenames used in Images/ directory of the zip archive,
98 /// e.g. "per_base_quality" for per_base_quality.png and per_base_quality.svg.
99 fn chart_image_name(&self) -> Option<&str> {
100 None
101 }
102
103 /// Return the alt text for this module's chart image, if it has one.
104 /// Modules that produce charts should override this to return Some("...").
105 /// The default `write_html_report` uses this to call `write_chart_and_table`
106 /// automatically, so modules with charts only need to implement this method
107 /// instead of overriding `write_html_report`.
108 fn chart_alt_text(&self) -> Option<&str> {
109 None
110 }
111
112 /// Generate the SVG chart content for this module, if applicable.
113 /// In Java, writeDefaultImage() renders the Swing JPanel to both SVG
114 /// and PNG. Here we generate SVG first, then convert to PNG via resvg.
115 fn generate_chart_svg(&self) -> Option<String> {
116 None
117 }
118
119 /// Write this module's section to the text report.
120 fn write_text_report(&self, writer: &mut dyn io::Write) -> io::Result<()>;
121
122 /// Write this module's HTML content (table and/or chart image) to the report.
123 ///
124 /// In Java, each module's makeReport() calls writeTable() which
125 /// calls writeXhtmlTable() then writeTextTable(). Some modules also call
126 /// writeDefaultImage() to embed a chart. The default implementation here
127 /// checks for `chart_alt_text()` -- if present, it renders the chart and table
128 /// via `write_chart_and_table`; otherwise it renders just an HTML table from
129 /// the text report output, matching writeXhtmlTable().
130 fn write_html_report(&self, writer: &mut dyn io::Write) -> io::Result<()> {
131 // Modules with charts show only the chart in HTML, not the data table.
132 // The data table only goes into fastqc_data.txt.
133 if let Some(alt_text) = self.chart_alt_text() {
134 return crate::report::html::write_chart(self, alt_text, writer);
135 }
136 // Default: render the text report data as an HTML table
137 let mut text_buf = Vec::new();
138 self.write_text_report(&mut text_buf)?;
139 let text = String::from_utf8(text_buf)
140 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
141 crate::report::html::write_default_html_table(&text, writer)
142 }
143}
144
145/// Create the standard set of QC modules based on configuration.
146///
147/// Module order matches `ModuleFactory.java` instantiation order,
148/// which determines the order they appear in the report.
149/// The order is: BasicStats, PerBaseQuality, PerTileQuality, PerSequenceQuality,
150/// PerBaseContent, PerSequenceGC, NContent, SequenceLength, DuplicationLevel,
151/// OverRepresentedSeqs, AdapterContent, KmerContent.
152///
153/// Note: DuplicationLevel is listed BEFORE OverRepresentedSeqs in the report,
154/// but shares data with it. In Java, OverRepresentedSeqs creates DuplicationLevel
155/// and adds it to the module list before itself.
156pub fn create_modules(config: &FastQCConfig, limits: &Limits) -> Vec<Box<dyn QCModule>> {
157 let mut modules: Vec<Box<dyn QCModule>> = Vec::new();
158
159 let ng = config.nogroup;
160 let eg = config.expgroup;
161
162 // Load adapter and contaminant lists
163 let adapters = config.load_adapters().unwrap_or_default();
164 let contaminants = config.load_contaminants().unwrap_or_default();
165
166 // 1. BasicStats
167 modules.push(Box::new(basic_stats::BasicStats::new(limits)));
168
169 // 2. PerBaseQualityScores
170 if limits.is_module_enabled("quality_base") {
171 modules.push(Box::new(per_base_quality::PerBaseQualityScores::new(
172 limits,
173 ng,
174 eg,
175 config.min_length,
176 )));
177 }
178
179 // 3. PerTileQualityScores
180 if limits.is_module_enabled("tile") {
181 modules.push(Box::new(per_tile_quality::PerTileQualityScores::new(
182 limits,
183 ng,
184 eg,
185 config.min_length,
186 )));
187 }
188
189 // 4. PerSequenceQualityScores
190 if limits.is_module_enabled("quality_sequence") {
191 modules.push(Box::new(
192 per_sequence_quality::PerSequenceQualityScores::new(limits),
193 ));
194 }
195
196 // 5. PerBaseSequenceContent
197 if limits.is_module_enabled("sequence") {
198 modules.push(Box::new(
199 per_base_sequence_content::PerBaseSequenceContent::new(
200 limits,
201 ng,
202 eg,
203 config.min_length,
204 ),
205 ));
206 }
207
208 // 6. PerSequenceGCContent
209 if limits.is_module_enabled("gc_sequence") {
210 modules.push(Box::new(gc_content::PerSequenceGCContent::new(limits)));
211 }
212
213 // 7. NContent
214 if limits.is_module_enabled("n_content") {
215 modules.push(Box::new(n_content::NContent::new(
216 limits,
217 ng,
218 eg,
219 config.min_length,
220 )));
221 }
222
223 // 8. SequenceLengthDistribution
224 if limits.is_module_enabled("sequence_length") {
225 modules.push(Box::new(
226 sequence_length_distribution::SequenceLengthDistribution::new(limits, ng),
227 ));
228 }
229
230 // 9 & 10. DuplicationLevel and OverRepresentedSeqs (shared data)
231 // OverRepresentedSeqs creates DuplicationLevel in its constructor.
232 // DuplicationLevel appears BEFORE OverRepresentedSeqs in the module list.
233 let shared_data = Arc::new(Mutex::new(overrepresented_seqs::OverRepresentedData::new()));
234
235 if limits.is_module_enabled("duplication") {
236 modules.push(Box::new(duplication_level::DuplicationLevel::new(
237 shared_data.clone(),
238 limits,
239 )));
240 }
241
242 if limits.is_module_enabled("overrepresented") {
243 modules.push(Box::new(overrepresented_seqs::OverRepresentedSeqs::new(
244 limits,
245 config.dup_length,
246 &contaminants,
247 shared_data.clone(),
248 )));
249 }
250
251 // 11. AdapterContent
252 if limits.is_module_enabled("adapter") {
253 modules.push(Box::new(adapter_content::AdapterContent::new(
254 limits,
255 &adapters,
256 ng,
257 eg,
258 config.min_length,
259 )));
260 }
261
262 // 12. KmerContent
263 // Kmer is ignored by default (limits.txt: kmer ignore 1)
264 if limits.is_module_enabled("kmer") {
265 modules.push(Box::new(kmer_content::KmerContent::new(
266 limits,
267 config.kmer_size,
268 ng,
269 eg,
270 config.min_length,
271 )));
272 }
273
274 modules
275}