Skip to main content

use_component/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Commonly used component primitives.
8pub mod prelude {
9    pub use crate::{
10        ComponentId, ComponentKind, ComponentKindParseError, ComponentTextError, ComponentValue,
11        ReferenceDesignator,
12    };
13}
14
15/// Errors returned by non-empty component text wrappers.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ComponentTextError {
18    /// The provided text was empty after trimming whitespace.
19    Empty,
20}
21
22impl fmt::Display for ComponentTextError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Empty => formatter.write_str("component text cannot be empty"),
26        }
27    }
28}
29
30impl Error for ComponentTextError {}
31
32/// A stable component identifier.
33#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct ComponentId(String);
35
36impl ComponentId {
37    /// Creates a component ID from non-empty text.
38    ///
39    /// # Errors
40    ///
41    /// Returns [`ComponentTextError::Empty`] when the trimmed value is empty.
42    pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
43        non_empty_component_text(value).map(Self)
44    }
45
46    /// Returns the stored identifier text.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        &self.0
50    }
51
52    /// Consumes the ID and returns the owned string.
53    #[must_use]
54    pub fn into_string(self) -> String {
55        self.0
56    }
57}
58
59impl AsRef<str> for ComponentId {
60    fn as_ref(&self) -> &str {
61        self.as_str()
62    }
63}
64
65impl fmt::Display for ComponentId {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter.write_str(self.as_str())
68    }
69}
70
71impl FromStr for ComponentId {
72    type Err = ComponentTextError;
73
74    fn from_str(value: &str) -> Result<Self, Self::Err> {
75        Self::new(value)
76    }
77}
78
79/// A reference designator such as `R1`, `C4`, `U2`, `D3`, `J1`, or `SW1`.
80#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct ReferenceDesignator(String);
82
83impl ReferenceDesignator {
84    /// Creates a reference designator from non-empty text.
85    ///
86    /// Casing is preserved; the value is not normalized beyond trimming edge whitespace.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`ComponentTextError::Empty`] when the trimmed value is empty.
91    pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
92        non_empty_component_text(value).map(Self)
93    }
94
95    /// Returns the reference designator text.
96    #[must_use]
97    pub fn as_str(&self) -> &str {
98        &self.0
99    }
100
101    /// Consumes the designator and returns the owned string.
102    #[must_use]
103    pub fn into_string(self) -> String {
104        self.0
105    }
106}
107
108impl AsRef<str> for ReferenceDesignator {
109    fn as_ref(&self) -> &str {
110        self.as_str()
111    }
112}
113
114impl fmt::Display for ReferenceDesignator {
115    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116        formatter.write_str(self.as_str())
117    }
118}
119
120impl FromStr for ReferenceDesignator {
121    type Err = ComponentTextError;
122
123    fn from_str(value: &str) -> Result<Self, Self::Err> {
124        Self::new(value)
125    }
126}
127
128/// A descriptive component value such as `10k`, `100nF`, or `STM32F4`.
129#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct ComponentValue(String);
131
132impl ComponentValue {
133    /// Creates a component value from non-empty text.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`ComponentTextError::Empty`] when the trimmed value is empty.
138    pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
139        non_empty_component_text(value).map(Self)
140    }
141
142    /// Returns the stored value text.
143    #[must_use]
144    pub fn as_str(&self) -> &str {
145        &self.0
146    }
147
148    /// Consumes the value and returns the owned string.
149    #[must_use]
150    pub fn into_string(self) -> String {
151        self.0
152    }
153}
154
155impl AsRef<str> for ComponentValue {
156    fn as_ref(&self) -> &str {
157        self.as_str()
158    }
159}
160
161impl fmt::Display for ComponentValue {
162    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
163        formatter.write_str(self.as_str())
164    }
165}
166
167impl FromStr for ComponentValue {
168    type Err = ComponentTextError;
169
170    fn from_str(value: &str) -> Result<Self, Self::Err> {
171        Self::new(value)
172    }
173}
174
175/// A small electronic component classification vocabulary.
176#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
177pub enum ComponentKind {
178    Resistor,
179    Capacitor,
180    Inductor,
181    Diode,
182    Transistor,
183    IntegratedCircuit,
184    Connector,
185    Switch,
186    Sensor,
187    PowerSupply,
188    Unknown,
189    Custom(String),
190}
191
192impl ComponentKind {
193    /// Creates a custom component kind from non-empty text.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`ComponentKindParseError::Empty`] when the trimmed value is empty.
198    pub fn custom(value: impl AsRef<str>) -> Result<Self, ComponentKindParseError> {
199        let trimmed = value.as_ref().trim();
200        if trimmed.is_empty() {
201            Err(ComponentKindParseError::Empty)
202        } else {
203            Ok(Self::Custom(trimmed.to_string()))
204        }
205    }
206}
207
208impl fmt::Display for ComponentKind {
209    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210        formatter.write_str(match self {
211            Self::Resistor => "resistor",
212            Self::Capacitor => "capacitor",
213            Self::Inductor => "inductor",
214            Self::Diode => "diode",
215            Self::Transistor => "transistor",
216            Self::IntegratedCircuit => "integrated-circuit",
217            Self::Connector => "connector",
218            Self::Switch => "switch",
219            Self::Sensor => "sensor",
220            Self::PowerSupply => "power-supply",
221            Self::Unknown => "unknown",
222            Self::Custom(value) => value.as_str(),
223        })
224    }
225}
226
227impl FromStr for ComponentKind {
228    type Err = ComponentKindParseError;
229
230    fn from_str(value: &str) -> Result<Self, Self::Err> {
231        let trimmed = value.trim();
232        if trimmed.is_empty() {
233            return Err(ComponentKindParseError::Empty);
234        }
235
236        match normalized_token(trimmed).as_str() {
237            "resistor" => Ok(Self::Resistor),
238            "capacitor" => Ok(Self::Capacitor),
239            "inductor" => Ok(Self::Inductor),
240            "diode" => Ok(Self::Diode),
241            "transistor" => Ok(Self::Transistor),
242            "integrated-circuit" | "ic" => Ok(Self::IntegratedCircuit),
243            "connector" => Ok(Self::Connector),
244            "switch" => Ok(Self::Switch),
245            "sensor" => Ok(Self::Sensor),
246            "power-supply" => Ok(Self::PowerSupply),
247            "unknown" => Ok(Self::Unknown),
248            _ => Ok(Self::Custom(trimmed.to_string())),
249        }
250    }
251}
252
253/// Errors returned while parsing component kinds.
254#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub enum ComponentKindParseError {
256    /// The kind text was empty after trimming whitespace.
257    Empty,
258}
259
260impl fmt::Display for ComponentKindParseError {
261    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262        match self {
263            Self::Empty => formatter.write_str("component kind cannot be empty"),
264        }
265    }
266}
267
268impl Error for ComponentKindParseError {}
269
270fn non_empty_component_text(value: impl AsRef<str>) -> Result<String, ComponentTextError> {
271    let trimmed = value.as_ref().trim();
272    if trimmed.is_empty() {
273        Err(ComponentTextError::Empty)
274    } else {
275        Ok(trimmed.to_string())
276    }
277}
278
279fn normalized_token(value: &str) -> String {
280    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
281}
282
283#[cfg(test)]
284mod tests {
285    use std::collections::BTreeSet;
286
287    use super::{ComponentKind, ComponentKindParseError, ComponentTextError, ReferenceDesignator};
288
289    #[test]
290    fn accepts_valid_reference_designators() -> Result<(), ComponentTextError> {
291        let reference = ReferenceDesignator::new("R1")?;
292
293        assert_eq!(reference.as_str(), "R1");
294        assert_eq!(reference.to_string(), "R1");
295        Ok(())
296    }
297
298    #[test]
299    fn rejects_empty_reference_designators() {
300        assert_eq!(
301            ReferenceDesignator::new("  "),
302            Err(ComponentTextError::Empty)
303        );
304    }
305
306    #[test]
307    fn displays_and_parses_component_kinds() -> Result<(), ComponentKindParseError> {
308        assert_eq!(
309            "resistor".parse::<ComponentKind>()?,
310            ComponentKind::Resistor
311        );
312        assert_eq!(
313            "Integrated Circuit".parse::<ComponentKind>()?,
314            ComponentKind::IntegratedCircuit
315        );
316        assert_eq!(ComponentKind::PowerSupply.to_string(), "power-supply");
317        Ok(())
318    }
319
320    #[test]
321    fn supports_custom_component_kinds() -> Result<(), ComponentKindParseError> {
322        let kind = ComponentKind::custom("fuse")?;
323
324        assert_eq!(kind, ComponentKind::Custom("fuse".to_string()));
325        assert_eq!("relay".parse::<ComponentKind>()?.to_string(), "relay");
326        Ok(())
327    }
328
329    #[test]
330    fn sorts_reference_designators_deterministically() -> Result<(), ComponentTextError> {
331        let references = BTreeSet::from([
332            ReferenceDesignator::new("R2")?,
333            ReferenceDesignator::new("R1")?,
334            ReferenceDesignator::new("C1")?,
335        ]);
336        let ordered: Vec<_> = references.iter().map(ReferenceDesignator::as_str).collect();
337
338        assert_eq!(ordered, vec!["C1", "R1", "R2"]);
339        Ok(())
340    }
341}