1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod 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}