Skip to main content

use_net_label/
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 net-label primitives.
8pub mod prelude {
9    pub use crate::{
10        GroundKind, GroundKindParseError, NetLabel, PowerRail, SignalName, TextLabelError,
11    };
12}
13
14/// Errors returned by non-empty label wrappers.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum TextLabelError {
17    /// The label was empty after trimming whitespace.
18    Empty,
19}
20
21impl fmt::Display for TextLabelError {
22    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23        match self {
24            Self::Empty => formatter.write_str("label text cannot be empty"),
25        }
26    }
27}
28
29impl Error for TextLabelError {}
30
31/// A non-empty net label.
32#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct NetLabel(String);
34
35impl NetLabel {
36    /// Creates a net label from non-empty text.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`TextLabelError::Empty`] when the trimmed value is empty.
41    pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
42        non_empty_text(value).map(Self)
43    }
44
45    /// Returns the label text.
46    #[must_use]
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50
51    /// Returns whether this label is one of the obvious common ground labels.
52    #[must_use]
53    pub fn is_ground(&self) -> bool {
54        matches!(
55            self.0.to_ascii_uppercase().as_str(),
56            "GND" | "AGND" | "DGND" | "CHASSIS_GND" | "EARTH_GND"
57        )
58    }
59
60    /// Returns whether this label is one of the obvious common power-like labels.
61    #[must_use]
62    pub fn is_power_like(&self) -> bool {
63        matches!(
64            self.0.to_ascii_uppercase().as_str(),
65            "VCC" | "VDD" | "VSS" | "VIN" | "VBAT" | "3V3" | "5V" | "1V8" | "12V"
66        )
67    }
68}
69
70impl AsRef<str> for NetLabel {
71    fn as_ref(&self) -> &str {
72        self.as_str()
73    }
74}
75
76impl fmt::Display for NetLabel {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        formatter.write_str(self.as_str())
79    }
80}
81
82impl FromStr for NetLabel {
83    type Err = TextLabelError;
84
85    fn from_str(value: &str) -> Result<Self, Self::Err> {
86        Self::new(value)
87    }
88}
89
90/// A descriptive signal name such as `SDA`, `SCL`, `CLK`, or `RESET`.
91#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
92pub struct SignalName(String);
93
94impl SignalName {
95    /// Creates a signal name from non-empty text.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`TextLabelError::Empty`] when the trimmed value is empty.
100    pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
101        non_empty_text(value).map(Self)
102    }
103
104    /// Returns the signal name text.
105    #[must_use]
106    pub fn as_str(&self) -> &str {
107        &self.0
108    }
109}
110
111impl AsRef<str> for SignalName {
112    fn as_ref(&self) -> &str {
113        self.as_str()
114    }
115}
116
117impl fmt::Display for SignalName {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str(self.as_str())
120    }
121}
122
123impl FromStr for SignalName {
124    type Err = TextLabelError;
125
126    fn from_str(value: &str) -> Result<Self, Self::Err> {
127        Self::new(value)
128    }
129}
130
131/// A descriptive power rail label such as `VCC`, `3V3`, or `VIN`.
132#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PowerRail(String);
134
135impl PowerRail {
136    /// Creates a power rail label from non-empty text.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`TextLabelError::Empty`] when the trimmed value is empty.
141    pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
142        non_empty_text(value).map(Self)
143    }
144
145    /// Returns the power rail text.
146    #[must_use]
147    pub fn as_str(&self) -> &str {
148        &self.0
149    }
150}
151
152impl AsRef<str> for PowerRail {
153    fn as_ref(&self) -> &str {
154        self.as_str()
155    }
156}
157
158impl fmt::Display for PowerRail {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter.write_str(self.as_str())
161    }
162}
163
164impl FromStr for PowerRail {
165    type Err = TextLabelError;
166
167    fn from_str(value: &str) -> Result<Self, Self::Err> {
168        Self::new(value)
169    }
170}
171
172/// Common ground label vocabulary.
173#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
174pub enum GroundKind {
175    Ground,
176    AnalogGround,
177    DigitalGround,
178    ChassisGround,
179    EarthGround,
180    Unknown,
181    Custom(String),
182}
183
184impl fmt::Display for GroundKind {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        formatter.write_str(match self {
187            Self::Ground => "ground",
188            Self::AnalogGround => "analog-ground",
189            Self::DigitalGround => "digital-ground",
190            Self::ChassisGround => "chassis-ground",
191            Self::EarthGround => "earth-ground",
192            Self::Unknown => "unknown",
193            Self::Custom(value) => value.as_str(),
194        })
195    }
196}
197
198impl FromStr for GroundKind {
199    type Err = GroundKindParseError;
200
201    fn from_str(value: &str) -> Result<Self, Self::Err> {
202        let trimmed = value.trim();
203        if trimmed.is_empty() {
204            return Err(GroundKindParseError::Empty);
205        }
206
207        match normalized_token(trimmed).as_str() {
208            "ground" | "gnd" => Ok(Self::Ground),
209            "analog-ground" | "agnd" => Ok(Self::AnalogGround),
210            "digital-ground" | "dgnd" => Ok(Self::DigitalGround),
211            "chassis-ground" => Ok(Self::ChassisGround),
212            "earth-ground" => Ok(Self::EarthGround),
213            "unknown" => Ok(Self::Unknown),
214            _ => Ok(Self::Custom(trimmed.to_string())),
215        }
216    }
217}
218
219/// Errors returned while parsing ground kinds.
220#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum GroundKindParseError {
222    /// The ground kind was empty after trimming whitespace.
223    Empty,
224}
225
226impl fmt::Display for GroundKindParseError {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        match self {
229            Self::Empty => formatter.write_str("ground kind cannot be empty"),
230        }
231    }
232}
233
234impl Error for GroundKindParseError {}
235
236fn non_empty_text(value: impl AsRef<str>) -> Result<String, TextLabelError> {
237    let trimmed = value.as_ref().trim();
238    if trimmed.is_empty() {
239        Err(TextLabelError::Empty)
240    } else {
241        Ok(trimmed.to_string())
242    }
243}
244
245fn normalized_token(value: &str) -> String {
246    value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
247}
248
249#[cfg(test)]
250mod tests {
251    use super::{GroundKind, GroundKindParseError, NetLabel, SignalName, TextLabelError};
252
253    #[test]
254    fn accepts_valid_net_labels() -> Result<(), TextLabelError> {
255        let label = NetLabel::new("3V3")?;
256
257        assert_eq!(label.as_str(), "3V3");
258        assert!(label.is_power_like());
259        Ok(())
260    }
261
262    #[test]
263    fn rejects_empty_net_labels() {
264        assert_eq!(NetLabel::new("  "), Err(TextLabelError::Empty));
265    }
266
267    #[test]
268    fn displays_and_parses_ground_kinds() -> Result<(), GroundKindParseError> {
269        assert_eq!("AGND".parse::<GroundKind>()?, GroundKind::AnalogGround);
270        assert_eq!(GroundKind::DigitalGround.to_string(), "digital-ground");
271        Ok(())
272    }
273
274    #[test]
275    fn constructs_signal_names() -> Result<(), TextLabelError> {
276        let signal = SignalName::new("SCL")?;
277
278        assert_eq!(signal.as_str(), "SCL");
279        assert_eq!(signal.to_string(), "SCL");
280        Ok(())
281    }
282
283    #[test]
284    fn preserves_common_label_text() -> Result<(), TextLabelError> {
285        let ground = NetLabel::new("GND")?;
286        let reset = NetLabel::new("RESET")?;
287
288        assert_eq!(ground.as_str(), "GND");
289        assert!(ground.is_ground());
290        assert_eq!(reset.as_str(), "RESET");
291        assert!(!reset.is_power_like());
292        Ok(())
293    }
294}