bio_forge/db/
mod.rs

1//! Internal database API exposing read-only views over residue templates.
2//!
3//! Callers obtain [`TemplateView`] handles keyed by template name, enabling topology and IO
4//! layers to iterate atoms, hydrogens, and bonds without cloning the underlying schema.
5
6mod loader;
7mod schema;
8mod store;
9
10use crate::model::types::{BondOrder, Element, Point, StandardResidue};
11
12/// Retrieves a template by its canonical name.
13///
14/// # Arguments
15///
16/// * `name` - Template identifier such as `"ALA"` or `"HOH"`.
17///
18/// # Returns
19///
20/// `Some(TemplateView)` when the template exists, otherwise `None`.
21pub fn get_template(name: &str) -> Option<TemplateView<'_>> {
22    store::get_store()
23        .templates_by_name
24        .get(name)
25        .map(TemplateView::new)
26}
27
28/// Lightweight wrapper granting read-only access to a stored template.
29#[derive(Debug, Clone, Copy)]
30pub struct TemplateView<'a> {
31    inner: &'a store::InternalTemplate,
32}
33
34impl<'a> TemplateView<'a> {
35    /// Creates a new view from the internal store entry.
36    ///
37    /// # Arguments
38    ///
39    /// * `inner` - Reference to the cached template.
40    pub fn new(inner: &'a store::InternalTemplate) -> Self {
41        Self { inner }
42    }
43
44    /// Returns the template's display name.
45    ///
46    /// # Returns
47    ///
48    /// The literal name string stored in the schema.
49    pub fn name(&self) -> &'a str {
50        &self.inner.schema.info.name
51    }
52
53    /// Reports the standard residue enum associated with the template.
54    ///
55    /// # Returns
56    ///
57    /// The [`StandardResidue`] discriminant stored in metadata.
58    pub fn standard_name(&self) -> StandardResidue {
59        self.inner.schema.info.standard_name
60    }
61
62    /// Returns the net integer charge of the residue.
63    ///
64    /// # Returns
65    ///
66    /// Signed charge in electrons.
67    pub fn charge(&self) -> i32 {
68        self.inner.schema.info.charge
69    }
70
71    /// Iterates heavy atoms with their elements and reference coordinates.
72    ///
73    /// # Returns
74    ///
75    /// An iterator yielding `(name, element, Point)` tuples preserving declaration order.
76    pub fn heavy_atoms(&self) -> impl Iterator<Item = (&'a str, Element, Point)> {
77        self.inner
78            .schema
79            .atoms
80            .iter()
81            .map(|a| (a.name.as_str(), a.element, Point::from(a.pos)))
82    }
83
84    /// Iterates hydrogens, their coordinates, and anchor names.
85    ///
86    /// # Returns
87    ///
88    /// An iterator producing `(name, Point, anchor_iter)` tuples where `anchor_iter`
89    /// traverses the heavy-atom anchor names as `&str`.
90    pub fn hydrogens(
91        &self,
92    ) -> impl Iterator<Item = (&'a str, Point, impl Iterator<Item = &'a str>)> {
93        self.inner.schema.hydrogens.iter().map(|h| {
94            (
95                h.name.as_str(),
96                Point::from(h.pos),
97                h.anchors.iter().map(|s| s.as_str()),
98            )
99        })
100    }
101
102    /// Iterates bonds as name pairs plus their bond order.
103    ///
104    /// # Returns
105    ///
106    /// An iterator over `(atom1, atom2, BondOrder)` tuples mirroring the schema definition.
107    pub fn bonds(&self) -> impl Iterator<Item = (&'a str, &'a str, BondOrder)> {
108        self.inner
109            .schema
110            .bonds
111            .iter()
112            .map(|b| (b.a1.as_str(), b.a2.as_str(), b.order))
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::model::types::{BondOrder, Element};
120
121    fn create_mock_template(
122        name: &str,
123        standard_name: StandardResidue,
124        charge: i32,
125        atoms: Vec<schema::TemplateHeavyAtom>,
126        hydrogens: Vec<schema::TemplateHydrogen>,
127        bonds: Vec<schema::TemplateBond>,
128    ) -> store::InternalTemplate {
129        let schema = schema::ResidueTemplateFile {
130            info: schema::TemplateInfo {
131                name: name.to_string(),
132                standard_name,
133                charge,
134            },
135            atoms,
136            hydrogens,
137            bonds,
138        };
139        store::InternalTemplate { schema }
140    }
141
142    fn create_simple_mock_template() -> store::InternalTemplate {
143        let atoms = vec![
144            schema::TemplateHeavyAtom {
145                name: "CA".to_string(),
146                element: Element::C,
147                pos: [0.0, 0.0, 0.0],
148            },
149            schema::TemplateHeavyAtom {
150                name: "CB".to_string(),
151                element: Element::C,
152                pos: [1.0, 0.0, 0.0],
153            },
154        ];
155
156        let hydrogens = vec![schema::TemplateHydrogen {
157            name: "HA".to_string(),
158            pos: [0.5, 1.0, 0.0],
159            anchors: vec!["CA".to_string()],
160        }];
161
162        let bonds = vec![schema::TemplateBond {
163            a1: "CA".to_string(),
164            a2: "CB".to_string(),
165            order: BondOrder::Single,
166        }];
167
168        create_mock_template("TEST", StandardResidue::ALA, 0, atoms, hydrogens, bonds)
169    }
170
171    #[test]
172    fn get_template_returns_none_for_nonexistent_template() {
173        let result = get_template("NONEXISTENT");
174        assert!(result.is_none());
175    }
176
177    #[test]
178    fn get_template_returns_some_for_existing_template() {
179        let result = get_template("ALA");
180        assert!(result.is_some());
181    }
182
183    #[test]
184    fn template_view_name_returns_correct_name() {
185        let mock_template = create_simple_mock_template();
186        let view = TemplateView::new(&mock_template);
187
188        assert_eq!(view.name(), "TEST");
189    }
190
191    #[test]
192    fn template_view_standard_name_returns_correct_standard_name() {
193        let mock_template = create_simple_mock_template();
194        let view = TemplateView::new(&mock_template);
195
196        assert_eq!(view.standard_name(), StandardResidue::ALA);
197    }
198
199    #[test]
200    fn template_view_charge_returns_correct_charge() {
201        let mock_template = create_simple_mock_template();
202        let view = TemplateView::new(&mock_template);
203
204        assert_eq!(view.charge(), 0);
205    }
206
207    #[test]
208    fn template_view_charge_returns_correct_charge_for_charged_template() {
209        let atoms = vec![schema::TemplateHeavyAtom {
210            name: "N".to_string(),
211            element: Element::N,
212            pos: [0.0, 0.0, 0.0],
213        }];
214        let mock_template =
215            create_mock_template("LYS", StandardResidue::LYS, 1, atoms, vec![], vec![]);
216        let view = TemplateView::new(&mock_template);
217
218        assert_eq!(view.charge(), 1);
219    }
220
221    #[test]
222    fn template_view_heavy_atoms_returns_correct_iterator() {
223        let mock_template = create_simple_mock_template();
224        let view = TemplateView::new(&mock_template);
225
226        let heavy_atoms: Vec<_> = view.heavy_atoms().collect();
227
228        assert_eq!(heavy_atoms.len(), 2);
229
230        assert_eq!(heavy_atoms[0].0, "CA");
231        assert_eq!(heavy_atoms[0].1, Element::C);
232        assert_eq!(heavy_atoms[0].2, Point::new(0.0, 0.0, 0.0));
233
234        assert_eq!(heavy_atoms[1].0, "CB");
235        assert_eq!(heavy_atoms[1].1, Element::C);
236        assert_eq!(heavy_atoms[1].2, Point::new(1.0, 0.0, 0.0));
237    }
238
239    #[test]
240    fn template_view_heavy_atoms_returns_empty_iterator_for_template_without_atoms() {
241        let mock_template =
242            create_mock_template("EMPTY", StandardResidue::GLY, 0, vec![], vec![], vec![]);
243        let view = TemplateView::new(&mock_template);
244
245        let heavy_atoms: Vec<_> = view.heavy_atoms().collect();
246
247        assert_eq!(heavy_atoms.len(), 0);
248    }
249
250    #[test]
251    fn template_view_hydrogens_returns_correct_iterator() {
252        let mock_template = create_simple_mock_template();
253        let view = TemplateView::new(&mock_template);
254
255        let hydrogens: Vec<_> = view.hydrogens().collect();
256
257        assert_eq!(hydrogens.len(), 1);
258
259        let (name, pos, _) = &hydrogens[0];
260        assert_eq!(*name, "HA");
261        assert_eq!(*pos, Point::new(0.5, 1.0, 0.0));
262    }
263
264    #[test]
265    fn template_view_hydrogens_returns_empty_iterator_for_template_without_hydrogens() {
266        let atoms = vec![schema::TemplateHeavyAtom {
267            name: "CA".to_string(),
268            element: Element::C,
269            pos: [0.0, 0.0, 0.0],
270        }];
271        let mock_template =
272            create_mock_template("NO_H", StandardResidue::GLY, 0, atoms, vec![], vec![]);
273        let view = TemplateView::new(&mock_template);
274
275        let hydrogens: Vec<_> = view.hydrogens().collect();
276
277        assert_eq!(hydrogens.len(), 0);
278    }
279
280    #[test]
281    fn template_view_hydrogens_handles_multiple_anchors() {
282        let atoms = vec![
283            schema::TemplateHeavyAtom {
284                name: "CA".to_string(),
285                element: Element::C,
286                pos: [0.0, 0.0, 0.0],
287            },
288            schema::TemplateHeavyAtom {
289                name: "CB".to_string(),
290                element: Element::C,
291                pos: [1.0, 0.0, 0.0],
292            },
293        ];
294
295        let hydrogens = vec![schema::TemplateHydrogen {
296            name: "HA".to_string(),
297            pos: [0.5, 1.0, 0.0],
298            anchors: vec!["CA".to_string(), "CB".to_string()],
299        }];
300
301        let mock_template = create_mock_template(
302            "MULTI_ANCHOR",
303            StandardResidue::ALA,
304            0,
305            atoms,
306            hydrogens,
307            vec![],
308        );
309        let view = TemplateView::new(&mock_template);
310
311        let hydrogens_vec: Vec<_> = view.hydrogens().collect();
312        assert_eq!(hydrogens_vec.len(), 1);
313
314        let (name, pos, _) = &hydrogens_vec[0];
315        assert_eq!(*name, "HA");
316        assert_eq!(*pos, Point::new(0.5, 1.0, 0.0));
317    }
318
319    #[test]
320    fn template_view_bonds_returns_correct_iterator() {
321        let mock_template = create_simple_mock_template();
322        let view = TemplateView::new(&mock_template);
323
324        let bonds: Vec<_> = view.bonds().collect();
325
326        assert_eq!(bonds.len(), 1);
327
328        let (a1, a2, order) = bonds[0];
329        assert_eq!(a1, "CA");
330        assert_eq!(a2, "CB");
331        assert_eq!(order, BondOrder::Single);
332    }
333
334    #[test]
335    fn template_view_bonds_returns_empty_iterator_for_template_without_bonds() {
336        let atoms = vec![schema::TemplateHeavyAtom {
337            name: "CA".to_string(),
338            element: Element::C,
339            pos: [0.0, 0.0, 0.0],
340        }];
341        let mock_template =
342            create_mock_template("NO_BONDS", StandardResidue::GLY, 0, atoms, vec![], vec![]);
343        let view = TemplateView::new(&mock_template);
344
345        let bonds: Vec<_> = view.bonds().collect();
346
347        assert_eq!(bonds.len(), 0);
348    }
349
350    #[test]
351    fn template_view_bonds_handles_different_bond_orders() {
352        let atoms = vec![
353            schema::TemplateHeavyAtom {
354                name: "C1".to_string(),
355                element: Element::C,
356                pos: [0.0, 0.0, 0.0],
357            },
358            schema::TemplateHeavyAtom {
359                name: "C2".to_string(),
360                element: Element::C,
361                pos: [1.0, 0.0, 0.0],
362            },
363            schema::TemplateHeavyAtom {
364                name: "N1".to_string(),
365                element: Element::N,
366                pos: [2.0, 0.0, 0.0],
367            },
368        ];
369
370        let bonds = vec![
371            schema::TemplateBond {
372                a1: "C1".to_string(),
373                a2: "C2".to_string(),
374                order: BondOrder::Single,
375            },
376            schema::TemplateBond {
377                a1: "C2".to_string(),
378                a2: "N1".to_string(),
379                order: BondOrder::Double,
380            },
381            schema::TemplateBond {
382                a1: "C1".to_string(),
383                a2: "N1".to_string(),
384                order: BondOrder::Triple,
385            },
386        ];
387
388        let mock_template =
389            create_mock_template("BONDS", StandardResidue::ALA, 0, atoms, vec![], bonds);
390        let view = TemplateView::new(&mock_template);
391
392        let bonds_vec: Vec<_> = view.bonds().collect();
393
394        assert_eq!(bonds_vec.len(), 3);
395        assert_eq!(bonds_vec[0].2, BondOrder::Single);
396        assert_eq!(bonds_vec[1].2, BondOrder::Double);
397        assert_eq!(bonds_vec[2].2, BondOrder::Triple);
398    }
399
400    #[test]
401    fn template_view_bonds_handles_aromatic_bonds() {
402        let atoms = vec![
403            schema::TemplateHeavyAtom {
404                name: "C1".to_string(),
405                element: Element::C,
406                pos: [0.0, 0.0, 0.0],
407            },
408            schema::TemplateHeavyAtom {
409                name: "C2".to_string(),
410                element: Element::C,
411                pos: [1.0, 0.0, 0.0],
412            },
413        ];
414
415        let bonds = vec![schema::TemplateBond {
416            a1: "C1".to_string(),
417            a2: "C2".to_string(),
418            order: BondOrder::Aromatic,
419        }];
420
421        let mock_template =
422            create_mock_template("AROMATIC", StandardResidue::PHE, 0, atoms, vec![], bonds);
423        let view = TemplateView::new(&mock_template);
424
425        let bonds_vec: Vec<_> = view.bonds().collect();
426
427        assert_eq!(bonds_vec.len(), 1);
428        assert_eq!(bonds_vec[0].2, BondOrder::Aromatic);
429    }
430
431    #[test]
432    fn template_view_clone_creates_identical_copy() {
433        let mock_template = create_simple_mock_template();
434        let view1 = TemplateView::new(&mock_template);
435        let view2 = view1;
436
437        assert_eq!(view1.name(), view2.name());
438        assert_eq!(view1.standard_name(), view2.standard_name());
439        assert_eq!(view1.charge(), view2.charge());
440
441        let atoms1: Vec<_> = view1.heavy_atoms().collect();
442        let atoms2: Vec<_> = view2.heavy_atoms().collect();
443        assert_eq!(atoms1, atoms2);
444
445        let bonds1: Vec<_> = view1.bonds().collect();
446        let bonds2: Vec<_> = view2.bonds().collect();
447        assert_eq!(bonds1, bonds2);
448    }
449
450    #[test]
451    fn template_view_debug_formatting() {
452        let mock_template = create_simple_mock_template();
453        let view = TemplateView::new(&mock_template);
454
455        let debug_str = format!("{:?}", view);
456        assert!(debug_str.contains("TemplateView"));
457    }
458
459    #[test]
460    fn get_template_with_real_database_templates() {
461        let templates_to_test = vec![
462            "ALA", "GLY", "VAL", "LEU", "ILE", "MET", "PHE", "PRO", "SER", "THR", "TYR", "CYS",
463            "ASN", "GLN", "ASP", "GLU", "LYS", "ARG", "HID", "HIE", "HIP", "HOH",
464        ];
465
466        for template_name in templates_to_test {
467            let result = get_template(template_name);
468            assert!(
469                result.is_some(),
470                "Template '{}' should exist",
471                template_name
472            );
473
474            let template = result.unwrap();
475            assert_eq!(template.name(), template_name);
476        }
477    }
478
479    #[test]
480    fn template_view_with_real_template_has_expected_properties() {
481        if let Some(ala_template) = get_template("ALA") {
482            assert_eq!(ala_template.name(), "ALA");
483            assert_eq!(ala_template.standard_name(), StandardResidue::ALA);
484            assert_eq!(ala_template.charge(), 0);
485
486            let heavy_atoms: Vec<_> = ala_template.heavy_atoms().collect();
487            assert!(!heavy_atoms.is_empty());
488
489            let bonds: Vec<_> = ala_template.bonds().collect();
490            assert!(!bonds.is_empty());
491        }
492    }
493
494    #[test]
495    fn template_view_with_real_template_water_properties() {
496        if let Some(hoh_template) = get_template("HOH") {
497            assert_eq!(hoh_template.name(), "HOH");
498            assert_eq!(hoh_template.standard_name(), StandardResidue::HOH);
499            assert_eq!(hoh_template.charge(), 0);
500
501            let heavy_atoms: Vec<_> = hoh_template.heavy_atoms().collect();
502            assert_eq!(heavy_atoms.len(), 1);
503            assert_eq!(heavy_atoms[0].1, Element::O);
504
505            let hydrogens: Vec<_> = hoh_template.hydrogens().collect();
506            assert!(!hydrogens.is_empty());
507        }
508    }
509}