1use std::fmt;
25
26const PROTEIN_ALPHABET: &str = "ACDEFGHIKLMNPQRSTVWYBJOUXZacdefghiklmnpqrstvwybjouxz-*";
34
35const DNA_ALPHABET: &str = "ACGTNRYSWKMBDHVacgtnryswkmbdhv";
37
38#[derive(Debug, Clone)]
42pub struct ProteinInput {
43 pub id: String,
45 pub sequence: String,
47}
48
49impl ProteinInput {
50 pub fn new(id: impl Into<String>, sequence: impl Into<String>) -> Result<Self, String> {
54 let id = id.into();
55 let sequence = sequence.into();
56 validate_protein_sequence(&sequence)?;
57 Ok(Self { id, sequence })
58 }
59
60 pub fn len(&self) -> usize {
62 self.sequence.len()
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.sequence.is_empty()
68 }
69}
70
71impl fmt::Display for ProteinInput {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "Protein[{}](len={})", self.id, self.sequence.len())
74 }
75}
76
77#[derive(Debug, Clone)]
81pub struct Modification {
82 pub position: usize,
84 pub ccd: String,
86}
87
88impl Modification {
89 pub fn new(position: usize, ccd: impl Into<String>) -> Self {
91 Self {
92 position,
93 ccd: ccd.into(),
94 }
95 }
96}
97
98impl fmt::Display for Modification {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 write!(f, "Mod[{}@pos{}]", self.ccd, self.position)
101 }
102}
103
104#[derive(Debug, Clone)]
108pub struct DNAInput {
109 pub id: String,
111 pub sequence: String,
113 pub modifications: Vec<Modification>,
115}
116
117impl DNAInput {
118 pub fn new(id: impl Into<String>, sequence: impl Into<String>) -> Result<Self, String> {
122 let id = id.into();
123 let sequence = sequence.into();
124 validate_dna_sequence(&sequence)?;
125 Ok(Self {
126 id,
127 sequence,
128 modifications: Vec::new(),
129 })
130 }
131
132 pub fn with_modifications(
134 id: impl Into<String>,
135 sequence: impl Into<String>,
136 modifications: Vec<Modification>,
137 ) -> Result<Self, String> {
138 let id = id.into();
139 let sequence = sequence.into();
140 validate_dna_sequence(&sequence)?;
141 Ok(Self {
142 id,
143 sequence,
144 modifications,
145 })
146 }
147
148 pub fn add_modification(mut self, modification: Modification) -> Self {
150 self.modifications.push(modification);
151 self
152 }
153
154 pub fn len(&self) -> usize {
156 self.sequence.len()
157 }
158
159 pub fn is_empty(&self) -> bool {
161 self.sequence.is_empty()
162 }
163}
164
165impl fmt::Display for DNAInput {
166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167 write!(
168 f,
169 "DNA[{}](len={}, mods={})",
170 self.id,
171 self.sequence.len(),
172 self.modifications.len()
173 )
174 }
175}
176
177#[derive(Debug, Clone)]
181pub struct LigandInput {
182 pub id: String,
184 pub ccd: Vec<String>,
186}
187
188impl LigandInput {
189 pub fn new(id: impl Into<String>, ccd: Vec<String>) -> Self {
191 Self { id: id.into(), ccd }
192 }
193
194 pub fn from_ccd(id: impl Into<String>, ccd: impl Into<String>) -> Self {
196 Self {
197 id: id.into(),
198 ccd: vec![ccd.into()],
199 }
200 }
201
202 pub fn num_components(&self) -> usize {
204 self.ccd.len()
205 }
206}
207
208impl fmt::Display for LigandInput {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 write!(f, "Ligand[{}](ccd={:?})", self.id, self.ccd)
211 }
212}
213
214#[derive(Debug, Clone)]
218pub enum ChainInput {
219 Protein(ProteinInput),
220 DNA(DNAInput),
221 Ligand(LigandInput),
222}
223
224impl ChainInput {
225 pub fn id(&self) -> &str {
227 match self {
228 ChainInput::Protein(p) => &p.id,
229 ChainInput::DNA(d) => &d.id,
230 ChainInput::Ligand(l) => &l.id,
231 }
232 }
233}
234
235impl fmt::Display for ChainInput {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 match self {
238 ChainInput::Protein(p) => write!(f, "{p}"),
239 ChainInput::DNA(d) => write!(f, "{d}"),
240 ChainInput::Ligand(l) => write!(f, "{l}"),
241 }
242 }
243}
244
245impl From<ProteinInput> for ChainInput {
246 fn from(p: ProteinInput) -> Self {
247 ChainInput::Protein(p)
248 }
249}
250
251impl From<DNAInput> for ChainInput {
252 fn from(d: DNAInput) -> Self {
253 ChainInput::DNA(d)
254 }
255}
256
257impl From<LigandInput> for ChainInput {
258 fn from(l: LigandInput) -> Self {
259 ChainInput::Ligand(l)
260 }
261}
262
263#[derive(Debug, Clone, Default)]
269pub struct StructurePredictionInput {
270 pub sequences: Vec<ChainInput>,
272}
273
274impl StructurePredictionInput {
275 pub fn new() -> Self {
277 Self::default()
278 }
279
280 pub fn add_chain(mut self, chain: impl Into<ChainInput>) -> Self {
282 self.sequences.push(chain.into());
283 self
284 }
285
286 pub fn add_protein(self, protein: ProteinInput) -> Self {
288 self.add_chain(protein)
289 }
290
291 pub fn add_dna(self, dna: DNAInput) -> Self {
293 self.add_chain(dna)
294 }
295
296 pub fn add_ligand(self, ligand: LigandInput) -> Self {
298 self.add_chain(ligand)
299 }
300
301 pub fn num_chains(&self) -> usize {
303 self.sequences.len()
304 }
305}
306
307impl fmt::Display for StructurePredictionInput {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 write!(f, "StructurePredictionInput(chains=[")?;
310 for (i, chain) in self.sequences.iter().enumerate() {
311 if i > 0 {
312 write!(f, ", ")?;
313 }
314 write!(f, "{chain}")?;
315 }
316 write!(f, "])")
317 }
318}
319
320pub fn validate_protein_sequence(sequence: &str) -> Result<(), String> {
324 if sequence.is_empty() {
325 return Err("Protein sequence must not be empty".to_string());
326 }
327 for (i, ch) in sequence.char_indices() {
328 if !PROTEIN_ALPHABET.contains(ch) {
329 return Err(format!(
330 "Invalid amino acid '{}' at position {} in protein sequence",
331 ch,
332 i + 1
333 ));
334 }
335 }
336 Ok(())
337}
338
339pub fn validate_dna_sequence(sequence: &str) -> Result<(), String> {
341 if sequence.is_empty() {
342 return Err("DNA sequence must not be empty".to_string());
343 }
344 for (i, ch) in sequence.char_indices() {
345 if !DNA_ALPHABET.contains(ch) {
346 return Err(format!(
347 "Invalid nucleotide '{}' at position {} in DNA sequence",
348 ch,
349 i + 1
350 ));
351 }
352 }
353 Ok(())
354}
355
356#[cfg(test)]
359mod tests {
360 use super::*;
361
362 #[test]
363 fn test_protein_input_valid() {
364 let p = ProteinInput::new("A", "ACDEFGHIKLMNPQRSTVWY").unwrap();
365 assert_eq!(p.id, "A");
366 assert_eq!(p.len(), 20);
367 assert!(!p.is_empty());
368 }
369
370 #[test]
371 fn test_protein_input_ambiguous_codes() {
372 ProteinInput::new("A", "BJOUXZ").expect("ambiguous codes should be valid");
374 }
375
376 #[test]
377 fn test_protein_input_invalid_char() {
378 let err = ProteinInput::new("A", "ACDE1FGHIK").unwrap_err();
379 assert!(err.contains("'1'"));
380 }
381
382 #[test]
383 fn test_protein_input_empty() {
384 assert!(ProteinInput::new("A", "").is_err());
385 }
386
387 #[test]
388 fn test_dna_input_valid() {
389 let d = DNAInput::new("B", "ACGTNRYSW").unwrap();
390 assert_eq!(d.id, "B");
391 assert_eq!(d.len(), 9);
392 }
393
394 #[test]
395 fn test_dna_input_invalid_char() {
396 let err = DNAInput::new("B", "ACGT1").unwrap_err();
397 assert!(err.contains("'1'"));
398 }
399
400 #[test]
401 fn test_dna_input_with_modification() {
402 let d = DNAInput::new("B", "ACGT")
403 .unwrap()
404 .add_modification(Modification::new(2, "5MC"));
405 assert_eq!(d.modifications.len(), 1);
406 assert_eq!(d.modifications[0].ccd, "5MC");
407 }
408
409 #[test]
410 fn test_ligand_from_ccd() {
411 let l = LigandInput::from_ccd("L", "ATP");
412 assert_eq!(l.id, "L");
413 assert_eq!(l.ccd, vec!["ATP".to_string()]);
414 assert_eq!(l.num_components(), 1);
415 }
416
417 #[test]
418 fn test_structure_prediction_input_builder() {
419 let protein = ProteinInput::new("A", "MKTAYIAK").unwrap();
420 let dna = DNAInput::new("B", "ACGTACGT").unwrap();
421 let ligand = LigandInput::from_ccd("L", "ATP");
422
423 let input = StructurePredictionInput::new()
424 .add_protein(protein)
425 .add_dna(dna)
426 .add_ligand(ligand);
427
428 assert_eq!(input.num_chains(), 3);
429 assert_eq!(input.sequences[0].id(), "A");
430 assert_eq!(input.sequences[1].id(), "B");
431 assert_eq!(input.sequences[2].id(), "L");
432 }
433
434 #[test]
435 fn test_display_protein() {
436 let p = ProteinInput::new("A", "MKTAYIAK").unwrap();
437 assert_eq!(format!("{p}"), "Protein[A](len=8)");
438 }
439
440 #[test]
441 fn test_display_dna() {
442 let d = DNAInput::new("B", "ACGT").unwrap();
443 assert_eq!(format!("{d}"), "DNA[B](len=4, mods=0)");
444 }
445
446 #[test]
447 fn test_display_ligand() {
448 let l = LigandInput::from_ccd("L", "ATP");
449 assert_eq!(format!("{l}"), "Ligand[L](ccd=[\"ATP\"])");
450 }
451
452 #[test]
453 fn test_display_structure_prediction_input() {
454 let input = StructurePredictionInput::new()
455 .add_protein(ProteinInput::new("A", "MKTAYIAK").unwrap());
456 let s = format!("{input}");
457 assert!(s.contains("StructurePredictionInput"));
458 assert!(s.contains("Protein[A]"));
459 }
460
461 #[test]
462 fn test_chain_input_id() {
463 let chain: ChainInput = ProteinInput::new("X", "ACDE").unwrap().into();
464 assert_eq!(chain.id(), "X");
465 }
466}