1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{
10 GroundKind, GroundKindParseError, NetLabel, PowerRail, SignalName, TextLabelError,
11 };
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum TextLabelError {
17 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct NetLabel(String);
34
35impl NetLabel {
36 pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
42 non_empty_text(value).map(Self)
43 }
44
45 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50
51 #[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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
92pub struct SignalName(String);
93
94impl SignalName {
95 pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
101 non_empty_text(value).map(Self)
102 }
103
104 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
133pub struct PowerRail(String);
134
135impl PowerRail {
136 pub fn new(value: impl AsRef<str>) -> Result<Self, TextLabelError> {
142 non_empty_text(value).map(Self)
143 }
144
145 #[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
221pub enum GroundKindParseError {
222 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}