Skip to main content

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}