Skip to main content

use_bond/
lib.rs

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