Skip to main content

use_molecule/
lib.rs

1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5//! Molecular identity primitives.
6
7mod atom_connection;
8mod atom_index;
9mod atom_label;
10mod error;
11mod molecular_atom;
12mod molecular_formula;
13mod molecule;
14mod molecule_builder;
15mod molecule_charge;
16mod molecule_kind;
17mod molecule_name;
18
19pub use atom_connection::AtomConnection;
20pub use atom_index::{AtomCount, AtomIndex};
21pub use atom_label::AtomLabel;
22pub use error::MoleculeValidationError;
23pub use molecular_atom::{MolecularAtom, MolecularAtomId};
24pub use molecular_formula::MolecularFormula;
25pub use molecule::Molecule;
26pub use molecule_builder::MoleculeBuilder;
27pub use molecule_charge::MoleculeCharge;
28pub use molecule_kind::MoleculeKind;
29pub use molecule_name::MoleculeName;
30
31#[cfg(test)]
32mod tests {
33    use use_chemical_formula::ChemicalFormula;
34
35    use super::{
36        AtomConnection, AtomCount, AtomIndex, AtomLabel, MolecularAtom, MolecularAtomId,
37        MolecularFormula, Molecule, MoleculeCharge, MoleculeKind, MoleculeName,
38        MoleculeValidationError,
39    };
40
41    fn formula(input: &str) -> ChemicalFormula {
42        ChemicalFormula::parse(input).expect("test formula should parse")
43    }
44
45    #[test]
46    fn creates_simple_molecule() {
47        let water = Molecule::new("water", formula("H2O")).expect("molecule should be valid");
48
49        assert_eq!(water.name().as_str(), "water");
50        assert_eq!(water.formula().to_string(), "H2O");
51        assert_eq!(water.atom_count(), 0);
52        assert!(water.atoms().is_empty());
53        assert!(water.connections().is_empty());
54    }
55
56    #[test]
57    fn validates_molecule_names() {
58        assert_eq!(
59            MoleculeName::new("   "),
60            Err(MoleculeValidationError::EmptyName)
61        );
62        assert_eq!(
63            Molecule::new("", formula("H2O")).map(|molecule| molecule.name().to_string()),
64            Err(MoleculeValidationError::EmptyName)
65        );
66        assert_eq!(
67            MoleculeName::new(" water ").map(|name| name.to_string()),
68            Ok(String::from("water"))
69        );
70    }
71
72    #[test]
73    fn assigns_and_displays_formula() {
74        let methane_formula = MolecularFormula::new(formula("CH4"));
75        let methane = Molecule::new("methane", methane_formula.clone().into_formula())
76            .expect("molecule should be valid");
77
78        assert_eq!(methane_formula.as_formula().to_string(), "CH4");
79        assert_eq!(methane.molecular_formula().to_string(), "CH4");
80        assert_eq!(methane.to_string(), "methane (CH4)");
81    }
82
83    #[test]
84    fn assigns_neutral_kind_and_charge() {
85        let oxygen = Molecule::new("oxygen", formula("O2"))
86            .expect("molecule should be valid")
87            .with_kind(MoleculeKind::Neutral)
88            .with_kind(MoleculeKind::Diatomic)
89            .with_kind(MoleculeKind::Diatomic)
90            .with_charge(MoleculeCharge::NEUTRAL);
91
92        assert_eq!(
93            oxygen.kinds(),
94            &[MoleculeKind::Neutral, MoleculeKind::Diatomic]
95        );
96        assert_eq!(oxygen.charge(), MoleculeCharge::NEUTRAL);
97        assert!(oxygen.charge().is_neutral());
98        assert_eq!(MoleculeCharge::new(1).to_string(), "+1");
99        assert_eq!(MoleculeKind::Polyatomic.to_string(), "polyatomic");
100    }
101
102    #[test]
103    fn supports_explicit_atoms_and_atom_ids() {
104        let oxygen_atom = MolecularAtom::new("O")
105            .expect("atom label should be valid")
106            .try_with_id("oxygen-1")
107            .expect("atom id should be valid");
108        let hydrogen = MolecularAtom::new("H").expect("atom label should be valid");
109        let water = Molecule::new("water", formula("H2O"))
110            .expect("molecule should be valid")
111            .with_atom(oxygen_atom.clone())
112            .with_atom(hydrogen.clone())
113            .with_atom(hydrogen);
114
115        assert_eq!(water.atom_count(), 3);
116        assert_eq!(water.atom_count_value(), AtomCount::new(3));
117        assert_eq!(water.atoms()[0], oxygen_atom);
118        assert_eq!(oxygen_atom.label().to_string(), "O");
119        assert_eq!(
120            oxygen_atom.id().map(MolecularAtomId::as_str),
121            Some("oxygen-1")
122        );
123    }
124
125    #[test]
126    fn exposes_atom_indices_and_connections() {
127        let connection = AtomConnection::new(AtomIndex::new(0), AtomIndex::new(1), Some(1))
128            .expect("connection should be valid");
129        let molecule = Molecule::new("hydrogen", formula("H2"))
130            .expect("molecule should be valid")
131            .with_atom(MolecularAtom::new("H").expect("atom label should be valid"))
132            .with_atom(MolecularAtom::new("H").expect("atom label should be valid"))
133            .try_with_connection(connection)
134            .expect("connection indices should be valid");
135
136        assert_eq!(AtomIndex::new(1).get(), 1);
137        assert_eq!(molecule.connections().len(), 1);
138        assert_eq!(molecule.connections()[0].from.get(), 0);
139        assert_eq!(molecule.connections()[0].to.get(), 1);
140        assert_eq!(molecule.connections()[0].order, Some(1));
141    }
142
143    #[test]
144    fn rejects_invalid_atom_labels_and_ids() {
145        assert_eq!(
146            AtomLabel::new(""),
147            Err(MoleculeValidationError::EmptyAtomLabel)
148        );
149        assert_eq!(
150            MolecularAtom::new("water"),
151            Err(MoleculeValidationError::InvalidAtomLabel(String::from(
152                "water"
153            )))
154        );
155        assert_eq!(
156            MolecularAtomId::new("  "),
157            Err(MoleculeValidationError::EmptyAtomId)
158        );
159    }
160
161    #[test]
162    fn rejects_invalid_atom_connections() {
163        assert_eq!(
164            AtomConnection::new(AtomIndex::new(0), AtomIndex::new(0), Some(1)),
165            Err(MoleculeValidationError::SelfConnection { index: 0 })
166        );
167        assert_eq!(
168            AtomConnection::new(AtomIndex::new(0), AtomIndex::new(1), Some(0)),
169            Err(MoleculeValidationError::ZeroConnectionOrder)
170        );
171
172        let connection = AtomConnection::new(AtomIndex::new(0), AtomIndex::new(2), None)
173            .expect("connection should be structurally valid");
174        let molecule = Molecule::new("water", formula("H2O"))
175            .expect("molecule should be valid")
176            .with_atom(MolecularAtom::new("O").expect("atom label should be valid"))
177            .with_atom(MolecularAtom::new("H").expect("atom label should be valid"));
178
179        assert_eq!(
180            molecule.try_with_connection(connection),
181            Err(MoleculeValidationError::InvalidConnectionIndex {
182                index: 2,
183                atom_count: 2,
184            })
185        );
186    }
187
188    #[test]
189    fn builds_molecules_with_builder() {
190        let molecule = Molecule::builder("water")
191            .formula(formula("H2O"))
192            .atom(MolecularAtom::new("O").expect("atom label should be valid"))
193            .atom(MolecularAtom::new("H").expect("atom label should be valid"))
194            .atom(MolecularAtom::new("H").expect("atom label should be valid"))
195            .connection(
196                AtomConnection::new(AtomIndex::new(0), AtomIndex::new(1), Some(1))
197                    .expect("connection should be valid"),
198            )
199            .kind(MoleculeKind::Neutral)
200            .build()
201            .expect("builder should produce a molecule");
202
203        assert_eq!(molecule.atom_count(), 3);
204        assert_eq!(molecule.connections().len(), 1);
205        assert_eq!(molecule.kinds(), &[MoleculeKind::Neutral]);
206        assert_eq!(
207            Molecule::builder("missing formula").build(),
208            Err(MoleculeValidationError::MissingFormula)
209        );
210    }
211}