///|
/// Validated source record. IDs are preserved; descriptions are not echoed in errors.
pub(all) struct Sample {
id : String
dna : Dna
} derive(Eq, Debug, ToJson)
///|
/// Strict wrapper: upstream recoverable parse issues become atomic failures.
pub fn read_fasta(
text : String,
topology? : Topology = Linear,
) -> Array[Sample] raise RestrictError {
if text.length() > 1000000 {
raise LimitExceeded("FASTA_BYTES")
}
let mut headers = 0
let mut lines = 0
for line in text.split("\n") {
lines = lines + 1
if lines > 10000 {
raise LimitExceeded("FASTA_LINES")
}
if line.trim().has_prefix(">") {
headers = headers + 1
}
if headers > 64 {
raise LimitExceeded("FASTA_RECORDS")
}
}
let parsed = @fasta.parse_fasta(text)
if !parsed.issues.is_empty() {
raise InvalidInput("FASTA_ISSUE_LINE_" + parsed.issues[0].line.to_string())
}
if parsed.records.is_empty() {
raise InvalidInput("FASTA_EMPTY")
}
let samples : Array[Sample] = []
let mut total = 0
for record in parsed.records {
if record.identifier.length() > 128 {
raise LimitExceeded("FASTA_ID")
}
let dna = Dna::new(record.sequence, topology~)
total = total + dna.length()
if total > 100000 {
raise LimitExceeded("FASTA_TOTAL_BASES")
}
samples.push({ id: record.identifier, dna })
}
samples
}
///|
/// Serialize the strict subset with wrapped sequence lines and no descriptions.
pub fn write_fasta(samples : Array[Sample]) -> String raise RestrictError {
if samples.is_empty() || samples.length() > 64 {
raise InvalidInput("FASTA_RECORDS")
}
let out = StringBuilder::new()
for sample in samples {
if sample.id.is_empty() {
raise InvalidInput("FASTA_ID")
}
for c in sample.id.iter() {
if c <= ' ' || c == '>' || c.to_int() > 126 {
raise InvalidInput("FASTA_ID")
}
}
out.write_string(">" + sample.id + "\n")
for i = 0; i < sample.dna.length(); i = i + 60 {
let end = (i + 60).min(sample.dna.length())
out.write_string(sample.dna.bases[i:end].to_owned() + "\n")
}
}
let text = out.to_string()
let _ = read_fasta(text)
text
}