1use std::fmt;
4use std::fs::File;
5use std::io::{self, BufWriter, Write};
6use std::path::Path;
7
8use crate::error::{Error, Result};
9use crate::format::{Compression, CompressionLevel, Format};
10use crate::record::Sequence;
11use crate::seq::Alphabet;
12
13pub const DEFAULT_LINE_WIDTH: usize = 60;
15
16pub struct FastxWriter<W: Write> {
36 inner: W,
37 format: Format,
38 line_width: Option<usize>,
39 validate: Option<Alphabet>,
40 written: u64,
41}
42
43impl<W: Write> FastxWriter<W> {
44 pub fn new(inner: W, format: Format) -> FastxWriter<W> {
49 FastxWriter {
50 inner,
51 format,
52 line_width: Some(DEFAULT_LINE_WIDTH),
53 validate: None,
54 written: 0,
55 }
56 }
57
58 pub fn line_width(mut self, width: usize) -> Self {
60 self.line_width = if width == 0 { None } else { Some(width) };
61 self
62 }
63
64 pub fn validate(mut self, alphabet: Alphabet) -> Self {
66 self.validate = Some(alphabet);
67 self
68 }
69
70 pub fn format(&self) -> Format {
72 self.format
73 }
74
75 pub fn records_written(&self) -> u64 {
77 self.written
78 }
79
80 pub fn write_record(&mut self, record: &Sequence) -> Result<()> {
82 if let Some(alphabet) = self.validate {
83 record.validate(alphabet)?;
84 }
85 match self.format {
86 Format::Fasta => record.write_fasta(&mut self.inner, self.line_width)?,
87 Format::Fastq => record.write_fastq(&mut self.inner)?,
88 }
89 self.written += 1;
90 Ok(())
91 }
92
93 pub fn write_all<'a, I>(&mut self, records: I) -> Result<()>
95 where
96 I: IntoIterator<Item = &'a Sequence>,
97 {
98 for record in records {
99 self.write_record(record)?;
100 }
101 Ok(())
102 }
103
104 pub fn write_fasta(&mut self, id: &str, description: Option<&str>, seq: &[u8]) -> Result<()> {
106 crate::record::check_writable_residues(seq, id)?;
107 self.write_header(b'>', id, description)?;
108 match self.line_width {
109 None => {
110 self.inner.write_all(seq)?;
111 self.inner.write_all(b"\n")?;
112 }
113 Some(width) => {
114 if seq.is_empty() {
115 self.inner.write_all(b"\n")?;
116 }
117 for chunk in seq.chunks(width) {
118 self.inner.write_all(chunk)?;
119 self.inner.write_all(b"\n")?;
120 }
121 }
122 }
123 self.written += 1;
124 Ok(())
125 }
126
127 pub fn write_fastq(
129 &mut self,
130 id: &str,
131 description: Option<&str>,
132 seq: &[u8],
133 quality: &[u8],
134 ) -> Result<()> {
135 if seq.len() != quality.len() {
136 return Err(Error::LengthMismatch {
137 id: id.to_string(),
138 seq: seq.len(),
139 quality: quality.len(),
140 });
141 }
142 crate::record::check_writable_residues(seq, id)?;
143 crate::record::check_writable_fastq_sequence(seq, id)?;
144 crate::record::check_writable_quality(quality, id)?;
145 self.write_header(b'@', id, description)?;
146 self.inner.write_all(seq)?;
147 self.inner.write_all(b"\n+\n")?;
148 self.inner.write_all(quality)?;
149 self.inner.write_all(b"\n")?;
150 self.written += 1;
151 Ok(())
152 }
153
154 fn write_header(&mut self, prefix: u8, id: &str, description: Option<&str>) -> Result<()> {
155 self.inner.write_all(&[prefix])?;
156 self.inner.write_all(id.as_bytes())?;
157 if let Some(description) = description.filter(|d| !d.is_empty()) {
158 self.inner.write_all(b" ")?;
159 self.inner.write_all(description.as_bytes())?;
160 }
161 self.inner.write_all(b"\n")?;
162 Ok(())
163 }
164
165 pub fn flush(&mut self) -> Result<()> {
167 self.inner.flush()?;
168 Ok(())
169 }
170
171 pub fn finish(mut self) -> Result<W> {
176 self.inner.flush()?;
177 Ok(self.inner)
178 }
179
180 pub fn get_ref(&self) -> &W {
182 &self.inner
183 }
184}
185
186impl<W: Write> fmt::Debug for FastxWriter<W> {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.debug_struct("FastxWriter")
189 .field("format", &self.format)
190 .field("line_width", &self.line_width)
191 .field("validate", &self.validate)
192 .field("records_written", &self.written)
193 .finish_non_exhaustive()
194 }
195}
196
197#[derive(Debug, Clone)]
211pub struct WriterBuilder {
212 format: Option<Format>,
213 line_width: Option<usize>,
214 compression: Option<Compression>,
215 level: CompressionLevel,
216 validate: Option<Alphabet>,
217 buffer_size: usize,
218 blocks_per_batch: Option<usize>,
219}
220
221impl Default for WriterBuilder {
222 fn default() -> Self {
223 WriterBuilder {
224 format: None,
225 line_width: Some(DEFAULT_LINE_WIDTH),
226 compression: None,
227 level: CompressionLevel::default(),
228 validate: None,
229 buffer_size: 128 * 1024,
230 blocks_per_batch: None,
231 }
232 }
233}
234
235impl WriterBuilder {
236 pub fn new() -> WriterBuilder {
238 WriterBuilder::default()
239 }
240
241 pub fn format(mut self, format: Format) -> Self {
243 self.format = Some(format);
244 self
245 }
246
247 pub fn line_width(mut self, width: usize) -> Self {
249 self.line_width = if width == 0 { None } else { Some(width) };
250 self
251 }
252
253 pub fn compression(mut self, compression: Compression) -> Self {
255 self.compression = Some(compression);
256 self
257 }
258
259 pub fn level(mut self, level: CompressionLevel) -> Self {
261 self.level = level;
262 self
263 }
264
265 pub fn validate(mut self, alphabet: Alphabet) -> Self {
267 self.validate = Some(alphabet);
268 self
269 }
270
271 pub fn buffer_size(mut self, bytes: usize) -> Self {
273 self.buffer_size = bytes;
274 self
275 }
276
277 pub fn blocks_per_batch(mut self, blocks: usize) -> Self {
284 self.blocks_per_batch = Some(blocks.max(1));
285 self
286 }
287
288 pub fn build<W: Write>(&self, inner: W) -> Result<FastxWriter<W>> {
290 let format = self.format.ok_or(Error::UnknownFormat {
291 hint: "no format given and no path to infer it from".to_string(),
292 })?;
293 Ok(FastxWriter {
294 inner,
295 format,
296 line_width: self.line_width,
297 validate: self.validate,
298 written: 0,
299 })
300 }
301
302 pub fn create<P: AsRef<Path>>(&self, path: P) -> Result<BoxedWriter> {
304 let path = path.as_ref();
305 let format = match self.format.or_else(|| Format::from_path(path)) {
306 Some(format) => format,
307 None => {
308 return Err(Error::UnknownFormat {
309 hint: format!("unrecognised extension in {}", path.display()),
310 })
311 }
312 };
313 let compression = self
318 .compression
319 .unwrap_or(match Compression::from_path(path) {
320 Compression::Gzip => Compression::Bgzf,
321 other => other,
322 });
323 let file = File::create(path)
324 .map_err(|e| Error::Io(io::Error::new(e.kind(), format!("{}: {e}", path.display()))))?;
325 let buffered = BufWriter::with_capacity(self.buffer_size, file);
326 let sink: Box<dyn Write + Send> = match compression {
327 Compression::None => Box::new(buffered),
328 Compression::Gzip => gzip_writer(buffered, self.level)?,
329 Compression::Bgzf => bgzf_writer(buffered, self.level, self.blocks_per_batch)?,
330 Compression::Zstd => zstd_writer(buffered, self.level)?,
331 };
332 Ok(FastxWriter {
333 inner: sink,
334 format,
335 line_width: self.line_width,
336 validate: self.validate,
337 written: 0,
338 })
339 }
340
341 pub fn stdout(&self) -> Result<BoxedWriter> {
343 let format = self.format.unwrap_or(Format::Fasta);
344 let sink: Box<dyn Write + Send> = match self.compression.unwrap_or(Compression::None) {
345 Compression::None => Box::new(BufWriter::with_capacity(self.buffer_size, io::stdout())),
346 Compression::Gzip => gzip_writer(
347 BufWriter::with_capacity(self.buffer_size, io::stdout()),
348 self.level,
349 )?,
350 Compression::Bgzf => bgzf_writer(
351 BufWriter::with_capacity(self.buffer_size, io::stdout()),
352 self.level,
353 self.blocks_per_batch,
354 )?,
355 Compression::Zstd => zstd_writer(
356 BufWriter::with_capacity(self.buffer_size, io::stdout()),
357 self.level,
358 )?,
359 };
360 Ok(FastxWriter {
361 inner: sink,
362 format,
363 line_width: self.line_width,
364 validate: self.validate,
365 written: 0,
366 })
367 }
368}
369
370#[cfg(feature = "gzip")]
371fn gzip_writer<W: Write + Send + 'static>(
372 sink: W,
373 level: CompressionLevel,
374) -> Result<Box<dyn Write + Send>> {
375 Ok(Box::new(flate2::write::GzEncoder::new(
376 sink,
377 flate2::Compression::new(level.0.min(9)),
378 )))
379}
380
381#[cfg(not(feature = "gzip"))]
382fn gzip_writer<W: Write + Send + 'static>(
383 _sink: W,
384 _level: CompressionLevel,
385) -> Result<Box<dyn Write + Send>> {
386 Err(Error::FeatureDisabled("gzip"))
387}
388
389#[cfg(feature = "gzip")]
390fn bgzf_writer<W: Write + Send + 'static>(
391 sink: W,
392 level: CompressionLevel,
393 blocks_per_batch: Option<usize>,
394) -> Result<Box<dyn Write + Send>> {
395 let mut writer = crate::bgzf::BgzfWriter::with_level(sink, level);
396 if let Some(blocks) = blocks_per_batch {
397 writer = writer.blocks_per_batch(blocks);
398 }
399 Ok(Box::new(writer))
400}
401
402#[cfg(not(feature = "gzip"))]
403fn bgzf_writer<W: Write + Send + 'static>(
404 _sink: W,
405 _level: CompressionLevel,
406 _blocks_per_batch: Option<usize>,
407) -> Result<Box<dyn Write + Send>> {
408 Err(Error::FeatureDisabled("gzip"))
409}
410
411#[cfg(feature = "zstd")]
418fn zstd_writer<W: Write + Send + 'static>(
419 sink: W,
420 level: CompressionLevel,
421) -> Result<Box<dyn Write + Send>> {
422 let zstd_level = match level.0 {
425 0 => 1,
426 level => (level.min(9) as i32 - 1) * 21 / 8 + 1,
427 };
428 let encoder = zstd::stream::write::Encoder::new(sink, zstd_level)?;
429 Ok(Box::new(encoder.auto_finish()))
430}
431
432#[cfg(not(feature = "zstd"))]
433fn zstd_writer<W: Write + Send + 'static>(
434 _sink: W,
435 _level: CompressionLevel,
436) -> Result<Box<dyn Write + Send>> {
437 Err(Error::FeatureDisabled("zstd"))
438}
439
440pub type BoxedWriter = FastxWriter<Box<dyn Write + Send>>;
442
443pub fn create<P: AsRef<Path>>(path: P) -> Result<BoxedWriter> {
452 WriterBuilder::default().create(path)
453}
454
455pub fn stdout(format: Format) -> Result<BoxedWriter> {
457 WriterBuilder::default().format(format).stdout()
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use crate::reader::FastxReader;
464
465 fn write_to_string(format: Format, records: &[Sequence], width: usize) -> String {
466 let mut out = Vec::new();
467 let mut writer = FastxWriter::new(&mut out, format).line_width(width);
468 writer.write_all(records).unwrap();
469 writer.flush().unwrap();
470 String::from_utf8(out).unwrap()
471 }
472
473 #[test]
474 fn writes_wrapped_fasta() {
475 let records = vec![Sequence::fasta("a", b"ACGTACGTAC").with_description("d")];
476 assert_eq!(
477 write_to_string(Format::Fasta, &records, 4),
478 ">a d\nACGT\nACGT\nAC\n"
479 );
480 assert_eq!(
481 write_to_string(Format::Fasta, &records, 0),
482 ">a d\nACGTACGTAC\n"
483 );
484 }
485
486 #[test]
487 fn drops_quality_when_writing_fasta() {
488 let records = vec![Sequence::fastq("a", b"ACGT", b"IIII").unwrap()];
489 assert_eq!(write_to_string(Format::Fasta, &records, 60), ">a\nACGT\n");
490 }
491
492 #[test]
493 fn refuses_fastq_without_quality() {
494 let mut out = Vec::new();
495 let mut writer = FastxWriter::new(&mut out, Format::Fastq);
496 let err = writer
497 .write_record(&Sequence::fasta("a", b"ACGT"))
498 .unwrap_err();
499 assert!(matches!(err, Error::MissingQuality { .. }));
500 }
501
502 #[test]
503 fn validates_on_demand() {
504 let mut out = Vec::new();
505 let mut writer = FastxWriter::new(&mut out, Format::Fasta).validate(Alphabet::Dna);
506 assert!(writer.write_record(&Sequence::fasta("a", b"ACGT")).is_ok());
507 let err = writer
508 .write_record(&Sequence::fasta("b", b"ACGX"))
509 .unwrap_err();
510 assert!(matches!(err, Error::InvalidByte { byte: b'X', .. }));
511 assert_eq!(writer.records_written(), 1);
512 }
513
514 #[test]
515 fn writes_parts_without_records() {
516 let mut out = Vec::new();
517 let mut writer = FastxWriter::new(&mut out, Format::Fastq);
518 writer
519 .write_fastq("r1", Some("desc"), b"ACGT", b"IIII")
520 .unwrap();
521 writer.write_fasta("r2", None, b"TTTT").unwrap();
522 writer.flush().unwrap();
523 assert_eq!(out, b"@r1 desc\nACGT\n+\nIIII\n>r2\nTTTT\n");
524
525 let mut out = Vec::new();
526 let mut writer = FastxWriter::new(&mut out, Format::Fastq);
527 assert!(writer.write_fastq("r", None, b"ACGT", b"II").is_err());
528 }
529
530 #[test]
531 fn round_trips_through_reader() {
532 let original: Vec<Sequence> = (0..50)
533 .map(|i| {
534 Sequence::fastq(
535 format!("read{i}"),
536 "ACGTN".repeat(i + 1).into_bytes(),
537 "IIIII".repeat(i + 1).into_bytes(),
538 )
539 .unwrap()
540 .with_description(format!("record number {i}"))
541 })
542 .collect();
543
544 let text = write_to_string(Format::Fastq, &original, 60);
545 let parsed: Vec<Sequence> = FastxReader::new(text.as_bytes())
546 .collect::<Result<Vec<_>>>()
547 .unwrap();
548 assert_eq!(parsed, original);
549
550 let fasta_input: Vec<Sequence> =
552 original.iter().cloned().map(Sequence::into_fasta).collect();
553 let text = write_to_string(Format::Fasta, &fasta_input, 7);
554 let parsed: Vec<Sequence> = FastxReader::new(text.as_bytes())
555 .collect::<Result<Vec<_>>>()
556 .unwrap();
557 assert_eq!(parsed, fasta_input);
558 }
559
560 #[test]
561 fn empty_sequence_round_trips() {
562 let records = vec![
563 Sequence::fasta("a", Vec::new()),
564 Sequence::fasta("b", b"AC"),
565 ];
566 let text = write_to_string(Format::Fasta, &records, 60);
567 assert_eq!(text, ">a\n\n>b\nAC\n");
568 let parsed: Vec<Sequence> = FastxReader::new(text.as_bytes())
569 .collect::<Result<Vec<_>>>()
570 .unwrap();
571 assert_eq!(parsed, records);
572 }
573
574 #[test]
575 fn builder_requires_a_format() {
576 let mut out = Vec::new();
577 assert!(matches!(
578 WriterBuilder::default().build(&mut out),
579 Err(Error::UnknownFormat { .. })
580 ));
581 assert!(WriterBuilder::default()
582 .format(Format::Fasta)
583 .build(&mut out)
584 .is_ok());
585 }
586}