use std::collections::HashSet;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use sbol3::{Document, Resource, SbolIdentified, SbolObject};
#[derive(Clone, Debug)]
pub struct FastaExporter {
line_width: usize,
}
impl Default for FastaExporter {
fn default() -> Self {
Self::new()
}
}
impl FastaExporter {
pub fn new() -> Self {
Self { line_width: 70 }
}
pub fn with_line_width(mut self, width: usize) -> Self {
self.line_width = width.max(1);
self
}
pub fn to_string(&self, document: &Document) -> String {
let mut out = String::new();
for record in self.records(document) {
out.push('>');
out.push_str(&record.header);
out.push('\n');
wrap_into(&record.elements, self.line_width, &mut out);
}
out
}
pub fn write<W: Write>(
&self,
document: &Document,
writer: &mut W,
) -> Result<ExportReport, ExportError> {
let text = self.to_string(document);
let records = text.bytes().filter(|byte| *byte == b'>').count();
writer
.write_all(text.as_bytes())
.map_err(|source| ExportError::Io {
path: PathBuf::from("<writer>"),
source,
})?;
Ok(ExportReport { records })
}
pub fn write_path(
&self,
document: &Document,
path: impl AsRef<Path>,
) -> Result<ExportReport, ExportError> {
let path = path.as_ref();
let file = File::create(path).map_err(|source| ExportError::Io {
path: path.to_path_buf(),
source,
})?;
let mut writer = BufWriter::new(file);
let report = self.write(document, &mut writer)?;
writer.flush().map_err(|source| ExportError::Io {
path: path.to_path_buf(),
source,
})?;
Ok(report)
}
fn records(&self, document: &Document) -> Vec<FastaRecord> {
let mut out = Vec::new();
let mut referenced: HashSet<Resource> = HashSet::new();
for component in document.components() {
for sequence_ref in &component.sequences {
let Some(SbolObject::Sequence(sequence)) = document.resolve(sequence_ref) else {
continue;
};
referenced.insert(sequence.identity.clone());
let Some(elements) = sequence.elements.as_deref() else {
continue;
};
if elements.is_empty() {
continue;
}
out.push(FastaRecord {
header: header(
component.display_id(),
&component.identity,
component.description(),
),
elements: elements.to_string(),
});
}
}
for sequence in document.sequences() {
if referenced.contains(&sequence.identity) {
continue;
}
let Some(elements) = sequence.elements.as_deref() else {
continue;
};
if elements.is_empty() {
continue;
}
out.push(FastaRecord {
header: header(
sequence.display_id(),
&sequence.identity,
sequence.description(),
),
elements: elements.to_string(),
});
}
out
}
}
struct FastaRecord {
header: String,
elements: String,
}
fn header(display_id: Option<&str>, identity: &Resource, description: Option<&str>) -> String {
let id = display_id
.map(str::to_string)
.unwrap_or_else(|| identity.to_string());
match description.map(str::trim).filter(|text| !text.is_empty()) {
Some(text) => format!("{id} {}", text.replace(['\n', '\r'], " ")),
None => id,
}
}
fn wrap_into(elements: &str, width: usize, out: &mut String) {
let chars: Vec<char> = elements.chars().collect();
for chunk in chars.chunks(width) {
out.extend(chunk.iter());
out.push('\n');
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct ExportReport {
pub records: usize,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ExportError {
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl std::fmt::Display for ExportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io { path, source } => write!(f, "failed to write {}: {source}", path.display()),
}
}
}
impl std::error::Error for ExportError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io { source, .. } => Some(source),
}
}
}
#[cfg(test)]
mod tests;