1#![forbid(unsafe_code)]
2#![allow(clippy::module_name_repetitions)]
3#![doc = include_str!("../README.md")]
4
5mod bond;
8mod bond_descriptor;
9mod bond_endpoint;
10mod bond_kind;
11mod bond_length;
12mod bond_order;
13mod bond_participant;
14mod bond_polarity;
15mod bond_strength;
16mod error;
17
18pub use bond::Bond;
19pub use bond_descriptor::BondDescriptor;
20pub use bond_endpoint::BondEndpoint;
21pub use bond_kind::BondKind;
22pub use bond_length::BondLength;
23pub use bond_order::{BondOrder, FractionalBondOrder};
24pub use bond_participant::BondParticipant;
25pub use bond_polarity::BondPolarity;
26pub use bond_strength::BondStrength;
27pub use error::BondValidationError;
28
29#[cfg(test)]
30mod tests {
31 use super::{
32 Bond, BondDescriptor, BondEndpoint, BondKind, BondLength, BondOrder, BondParticipant,
33 BondPolarity, BondStrength, BondValidationError, FractionalBondOrder,
34 };
35
36 fn endpoint(label: &str) -> BondEndpoint {
37 BondEndpoint::new(label).expect("endpoint should be valid")
38 }
39
40 #[test]
41 fn creates_simple_covalent_bond() {
42 let bond = Bond::new(BondKind::Covalent).with_order(BondOrder::Single);
43
44 assert_eq!(bond.kind(), BondKind::Covalent);
45 assert_eq!(bond.order(), Some(BondOrder::Single));
46 assert!(bond.endpoints().is_empty());
47 }
48
49 #[test]
50 fn creates_common_bond_orders() {
51 assert_eq!(BondOrder::Single.to_string(), "single");
52 assert_eq!(BondOrder::Double.to_string(), "double");
53 assert_eq!(BondOrder::Triple.to_string(), "triple");
54 assert_eq!(BondOrder::Aromatic.to_string(), "aromatic");
55 assert_eq!(
56 BondOrder::Fractional(
57 FractionalBondOrder::new(3, 2).expect("fractional order should be valid")
58 )
59 .to_string(),
60 "3/2"
61 );
62 }
63
64 #[test]
65 fn creates_ionic_and_hydrogen_bonds() {
66 let ionic = Bond::new(BondKind::Ionic).with_polarity(BondPolarity::Ionic);
67 let hydrogen = Bond::new(BondKind::Hydrogen).with_strength(BondStrength::Weak);
68
69 assert_eq!(ionic.kind(), BondKind::Ionic);
70 assert_eq!(ionic.polarity(), Some(BondPolarity::Ionic));
71 assert_eq!(hydrogen.kind(), BondKind::Hydrogen);
72 assert_eq!(hydrogen.strength(), Some(BondStrength::Weak));
73 }
74
75 #[test]
76 fn validates_endpoint_labels() {
77 assert_eq!(
78 BondEndpoint::new(" O ").map(|endpoint| endpoint.to_string()),
79 Ok(String::from("O"))
80 );
81 assert_eq!(
82 BondEndpoint::new(" "),
83 Err(BondValidationError::EmptyEndpointLabel)
84 );
85 }
86
87 #[test]
88 fn creates_endpoint_pair_bonds() {
89 let bond = Bond::between(endpoint("O"), endpoint("H"), BondKind::Covalent)
90 .with_order(BondOrder::Single);
91
92 assert_eq!(bond.endpoints().len(), 2);
93 assert_eq!(bond.endpoints()[0].as_str(), "O");
94 assert_eq!(bond.endpoints()[1].as_str(), "H");
95 assert_eq!(bond.to_string(), "O-H covalent bond (single)");
96 }
97
98 #[test]
99 fn assigns_polarity_independently_from_kind() {
100 let bond = Bond::new(BondKind::Covalent).with_polarity(BondPolarity::Polar);
101
102 assert_eq!(bond.kind(), BondKind::Covalent);
103 assert_eq!(bond.polarity(), Some(BondPolarity::Polar));
104 assert_eq!(BondPolarity::Nonpolar.to_string(), "nonpolar");
105 }
106
107 #[test]
108 fn assigns_strength_independently_from_kind() {
109 let bond = Bond::new(BondKind::VanDerWaals).with_strength(BondStrength::Weak);
110
111 assert_eq!(bond.kind(), BondKind::VanDerWaals);
112 assert_eq!(bond.strength(), Some(BondStrength::Weak));
113 assert_eq!(BondStrength::VeryStrong.to_string(), "very strong");
114 }
115
116 #[test]
117 fn represents_optional_bond_length() {
118 let length = BondLength::new(96.0, "pm").expect("length should be valid");
119 let bond = Bond::new(BondKind::Covalent).with_length(length.clone());
120
121 assert_eq!(bond.length(), Some(&length));
122 assert_eq!(length.value(), 96.0);
123 assert_eq!(length.unit(), "pm");
124 assert_eq!(length.to_string(), "96 pm");
125 }
126
127 #[test]
128 fn rejects_invalid_length_values() {
129 assert_eq!(
130 BondLength::new(f64::NAN, "pm"),
131 Err(BondValidationError::NonFiniteBondLength)
132 );
133 assert_eq!(
134 BondLength::new(0.0, "pm"),
135 Err(BondValidationError::NonPositiveBondLength)
136 );
137 assert_eq!(
138 BondLength::new(96.0, ""),
139 Err(BondValidationError::EmptyLengthUnit)
140 );
141 }
142
143 #[test]
144 fn rejects_invalid_empty_endpoint_labels() {
145 assert_eq!(
146 BondEndpoint::try_from(""),
147 Err(BondValidationError::EmptyEndpointLabel)
148 );
149 }
150
151 #[test]
152 fn formats_bonds_predictably() {
153 assert_eq!(Bond::new(BondKind::Covalent).to_string(), "covalent bond");
154 assert_eq!(
155 Bond::between(endpoint("C"), endpoint("C"), BondKind::Aromatic)
156 .with_order(BondOrder::Aromatic)
157 .to_string(),
158 "C-C aromatic bond (aromatic)"
159 );
160 assert_eq!(BondKind::LondonDispersion.to_string(), "London dispersion");
161 }
162
163 #[test]
164 fn validates_fractional_bond_orders() {
165 let order = FractionalBondOrder::new(3, 2).expect("fractional order should be valid");
166
167 assert_eq!(order.numerator(), 3);
168 assert_eq!(order.denominator(), 2);
169 assert_eq!(order.to_string(), "3/2");
170 assert_eq!(
171 FractionalBondOrder::new(0, 2),
172 Err(BondValidationError::ZeroFractionalBondOrderNumerator)
173 );
174 assert_eq!(
175 FractionalBondOrder::new(3, 0),
176 Err(BondValidationError::ZeroFractionalBondOrderDenominator)
177 );
178 }
179
180 #[test]
181 fn assigns_descriptors_participants_and_angle_labels() {
182 let descriptor = BondDescriptor::new("sigma").expect("descriptor should be valid");
183 let participant = BondParticipant::new("ligand").expect("participant should be valid");
184 let bond = Bond::new(BondKind::Coordinate)
185 .with_descriptor(descriptor.clone())
186 .with_descriptor(descriptor.clone())
187 .with_participant(participant.clone())
188 .try_with_angle_label("donor-metal-acceptor")
189 .expect("angle label should be valid");
190
191 assert_eq!(bond.descriptors(), &[descriptor]);
192 assert_eq!(bond.participants(), &[participant]);
193 assert_eq!(
194 bond.angle_label().map(BondDescriptor::as_str),
195 Some("donor-metal-acceptor")
196 );
197 assert_eq!(
198 Bond::new(BondKind::Covalent).try_with_angle_label(" "),
199 Err(BondValidationError::EmptyAngleLabel)
200 );
201 }
202}