1use std::collections::HashSet;
10use std::fs::File;
11use std::io::{BufWriter, Write};
12use std::path::{Path, PathBuf};
13
14use sbol3::{Document, Resource, SbolIdentified, SbolObject};
15
16#[derive(Clone, Debug)]
23pub struct FastaExporter {
24 line_width: usize,
25}
26
27impl Default for FastaExporter {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl FastaExporter {
34 pub fn new() -> Self {
36 Self { line_width: 70 }
37 }
38
39 pub fn with_line_width(mut self, width: usize) -> Self {
42 self.line_width = width.max(1);
43 self
44 }
45
46 pub fn to_string(&self, document: &Document) -> String {
48 let mut out = String::new();
49 for record in self.records(document) {
50 out.push('>');
51 out.push_str(&record.header);
52 out.push('\n');
53 wrap_into(&record.elements, self.line_width, &mut out);
54 }
55 out
56 }
57
58 pub fn write<W: Write>(
60 &self,
61 document: &Document,
62 writer: &mut W,
63 ) -> Result<ExportReport, ExportError> {
64 let text = self.to_string(document);
65 let records = text.bytes().filter(|byte| *byte == b'>').count();
66 writer
67 .write_all(text.as_bytes())
68 .map_err(|source| ExportError::Io {
69 path: PathBuf::from("<writer>"),
70 source,
71 })?;
72 Ok(ExportReport { records })
73 }
74
75 pub fn write_path(
77 &self,
78 document: &Document,
79 path: impl AsRef<Path>,
80 ) -> Result<ExportReport, ExportError> {
81 let path = path.as_ref();
82 let file = File::create(path).map_err(|source| ExportError::Io {
83 path: path.to_path_buf(),
84 source,
85 })?;
86 let mut writer = BufWriter::new(file);
87 let report = self.write(document, &mut writer)?;
88 writer.flush().map_err(|source| ExportError::Io {
89 path: path.to_path_buf(),
90 source,
91 })?;
92 Ok(report)
93 }
94
95 fn records(&self, document: &Document) -> Vec<FastaRecord> {
96 let mut out = Vec::new();
97 let mut referenced: HashSet<Resource> = HashSet::new();
98
99 for component in document.components() {
100 for sequence_ref in &component.sequences {
101 let Some(SbolObject::Sequence(sequence)) = document.resolve(sequence_ref) else {
102 continue;
103 };
104 referenced.insert(sequence.identity.clone());
105 let Some(elements) = sequence.elements.as_deref() else {
106 continue;
107 };
108 if elements.is_empty() {
109 continue;
110 }
111 out.push(FastaRecord {
112 header: header(
113 component.display_id(),
114 &component.identity,
115 component.description(),
116 ),
117 elements: elements.to_string(),
118 });
119 }
120 }
121
122 for sequence in document.sequences() {
123 if referenced.contains(&sequence.identity) {
124 continue;
125 }
126 let Some(elements) = sequence.elements.as_deref() else {
127 continue;
128 };
129 if elements.is_empty() {
130 continue;
131 }
132 out.push(FastaRecord {
133 header: header(
134 sequence.display_id(),
135 &sequence.identity,
136 sequence.description(),
137 ),
138 elements: elements.to_string(),
139 });
140 }
141
142 out
143 }
144}
145
146struct FastaRecord {
147 header: String,
148 elements: String,
149}
150
151fn header(display_id: Option<&str>, identity: &Resource, description: Option<&str>) -> String {
154 let id = display_id
155 .map(str::to_string)
156 .unwrap_or_else(|| identity.to_string());
157 match description.map(str::trim).filter(|text| !text.is_empty()) {
158 Some(text) => format!("{id} {}", text.replace(['\n', '\r'], " ")),
159 None => id,
160 }
161}
162
163fn wrap_into(elements: &str, width: usize, out: &mut String) {
165 let chars: Vec<char> = elements.chars().collect();
166 for chunk in chars.chunks(width) {
167 out.extend(chunk.iter());
168 out.push('\n');
169 }
170}
171
172#[derive(Clone, Debug, Default)]
174#[non_exhaustive]
175pub struct ExportReport {
176 pub records: usize,
178}
179
180#[derive(Debug)]
182#[non_exhaustive]
183pub enum ExportError {
184 Io {
186 path: PathBuf,
188 source: std::io::Error,
190 },
191}
192
193impl std::fmt::Display for ExportError {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 match self {
196 Self::Io { path, source } => write!(f, "failed to write {}: {source}", path.display()),
197 }
198 }
199}
200
201impl std::error::Error for ExportError {
202 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
203 match self {
204 Self::Io { source, .. } => Some(source),
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests;