Skip to main content

use_bond/
bond.rs

1use std::fmt;
2
3use crate::{
4    BondDescriptor, BondEndpoint, BondKind, BondLength, BondOrder, BondParticipant, BondPolarity,
5    BondStrength, BondValidationError,
6};
7
8/// A chemical bond identity with optional structural descriptors.
9#[derive(Clone, Debug, PartialEq)]
10pub struct Bond {
11    kind: BondKind,
12    order: Option<BondOrder>,
13    endpoints: Vec<BondEndpoint>,
14    participants: Vec<BondParticipant>,
15    polarity: Option<BondPolarity>,
16    strength: Option<BondStrength>,
17    length: Option<BondLength>,
18    angle_label: Option<BondDescriptor>,
19    descriptors: Vec<BondDescriptor>,
20}
21
22impl Bond {
23    /// Creates a bond with a kind label.
24    #[must_use]
25    pub fn new(kind: BondKind) -> Self {
26        Self {
27            kind,
28            order: None,
29            endpoints: Vec::new(),
30            participants: Vec::new(),
31            polarity: None,
32            strength: None,
33            length: None,
34            angle_label: None,
35            descriptors: Vec::new(),
36        }
37    }
38
39    /// Creates a bond between two validated endpoint references.
40    #[must_use]
41    pub fn between(endpoint_a: BondEndpoint, endpoint_b: BondEndpoint, kind: BondKind) -> Self {
42        Self::new(kind)
43            .with_endpoint(endpoint_a)
44            .with_endpoint(endpoint_b)
45    }
46
47    /// Returns the bond kind.
48    #[must_use]
49    pub const fn kind(&self) -> BondKind {
50        self.kind
51    }
52
53    /// Returns the optional bond order.
54    #[must_use]
55    pub const fn order(&self) -> Option<BondOrder> {
56        self.order
57    }
58
59    /// Returns endpoint references in insertion order.
60    #[must_use]
61    pub fn endpoints(&self) -> &[BondEndpoint] {
62        &self.endpoints
63    }
64
65    /// Returns participant references in insertion order.
66    #[must_use]
67    pub fn participants(&self) -> &[BondParticipant] {
68        &self.participants
69    }
70
71    /// Returns the optional polarity label.
72    #[must_use]
73    pub const fn polarity(&self) -> Option<BondPolarity> {
74        self.polarity
75    }
76
77    /// Returns the optional strength label.
78    #[must_use]
79    pub const fn strength(&self) -> Option<BondStrength> {
80        self.strength
81    }
82
83    /// Returns the optional bond length.
84    #[must_use]
85    pub const fn length(&self) -> Option<&BondLength> {
86        self.length.as_ref()
87    }
88
89    /// Returns the optional angle label or reference.
90    #[must_use]
91    pub const fn angle_label(&self) -> Option<&BondDescriptor> {
92        self.angle_label.as_ref()
93    }
94
95    /// Returns lightweight descriptors in insertion order.
96    #[must_use]
97    pub fn descriptors(&self) -> &[BondDescriptor] {
98        &self.descriptors
99    }
100
101    /// Assigns a bond order.
102    #[must_use]
103    pub const fn with_order(mut self, order: BondOrder) -> Self {
104        self.order = Some(order);
105        self
106    }
107
108    /// Adds an endpoint reference.
109    #[must_use]
110    pub fn with_endpoint(mut self, endpoint: BondEndpoint) -> Self {
111        self.endpoints.push(endpoint);
112        self
113    }
114
115    /// Adds an endpoint reference after validation.
116    ///
117    /// # Errors
118    ///
119    /// Returns [`BondValidationError::EmptyEndpointLabel`] when `endpoint` is empty after trimming.
120    pub fn try_with_endpoint(self, endpoint: &str) -> Result<Self, BondValidationError> {
121        Ok(self.with_endpoint(BondEndpoint::new(endpoint)?))
122    }
123
124    /// Adds a participant reference if it is not already present.
125    #[must_use]
126    pub fn with_participant(mut self, participant: BondParticipant) -> Self {
127        if !self.participants.contains(&participant) {
128            self.participants.push(participant);
129        }
130        self
131    }
132
133    /// Adds a participant reference after validation.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`BondValidationError::EmptyParticipantLabel`] when `participant` is empty after
138    /// trimming.
139    pub fn try_with_participant(self, participant: &str) -> Result<Self, BondValidationError> {
140        Ok(self.with_participant(BondParticipant::new(participant)?))
141    }
142
143    /// Assigns a polarity label.
144    #[must_use]
145    pub const fn with_polarity(mut self, polarity: BondPolarity) -> Self {
146        self.polarity = Some(polarity);
147        self
148    }
149
150    /// Assigns a strength label.
151    #[must_use]
152    pub const fn with_strength(mut self, strength: BondStrength) -> Self {
153        self.strength = Some(strength);
154        self
155    }
156
157    /// Assigns a bond length.
158    #[must_use]
159    pub fn with_length(mut self, length: BondLength) -> Self {
160        self.length = Some(length);
161        self
162    }
163
164    /// Assigns a bond length after validation.
165    ///
166    /// # Errors
167    ///
168    /// Returns [`BondValidationError`] when the length value is not finite, is not positive, or the
169    /// unit is empty after trimming.
170    pub fn try_with_length(self, value: f64, unit: &str) -> Result<Self, BondValidationError> {
171        Ok(self.with_length(BondLength::new(value, unit)?))
172    }
173
174    /// Assigns an angle label or reference.
175    #[must_use]
176    pub fn with_angle_label(mut self, angle_label: BondDescriptor) -> Self {
177        self.angle_label = Some(angle_label);
178        self
179    }
180
181    /// Assigns an angle label or reference after validation.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`BondValidationError::EmptyAngleLabel`] when `angle_label` is empty after trimming.
186    pub fn try_with_angle_label(self, angle_label: &str) -> Result<Self, BondValidationError> {
187        let trimmed = angle_label.trim();
188        if trimmed.is_empty() {
189            return Err(BondValidationError::EmptyAngleLabel);
190        }
191
192        Ok(self.with_angle_label(BondDescriptor::new(trimmed)?))
193    }
194
195    /// Adds a descriptor if it is not already present.
196    #[must_use]
197    pub fn with_descriptor(mut self, descriptor: BondDescriptor) -> Self {
198        if !self.descriptors.contains(&descriptor) {
199            self.descriptors.push(descriptor);
200        }
201        self
202    }
203
204    /// Adds a descriptor after validation.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`BondValidationError::EmptyDescriptor`] when `descriptor` is empty after trimming.
209    pub fn try_with_descriptor(self, descriptor: &str) -> Result<Self, BondValidationError> {
210        Ok(self.with_descriptor(BondDescriptor::new(descriptor)?))
211    }
212}
213
214impl fmt::Display for Bond {
215    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216        if let Some((first, rest)) = self.endpoints.split_first() {
217            write!(formatter, "{first}")?;
218            for endpoint in rest {
219                write!(formatter, "-{endpoint}")?;
220            }
221            write!(formatter, " {} bond", self.kind)?;
222        } else {
223            write!(formatter, "{} bond", self.kind)?;
224        }
225
226        if let Some(order) = self.order {
227            write!(formatter, " ({order})")?;
228        }
229
230        Ok(())
231    }
232}