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 ComponentId, ComponentKind, ComponentKindParseError, ComponentTextError, ComponentValue,
11 ReferenceDesignator,
12 };
13}
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ComponentTextError {
18 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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34pub struct ComponentId(String);
35
36impl ComponentId {
37 pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
43 non_empty_component_text(value).map(Self)
44 }
45
46 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct ReferenceDesignator(String);
82
83impl ReferenceDesignator {
84 pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
92 non_empty_component_text(value).map(Self)
93 }
94
95 #[must_use]
97 pub fn as_str(&self) -> &str {
98 &self.0
99 }
100
101 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
130pub struct ComponentValue(String);
131
132impl ComponentValue {
133 pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
139 non_empty_component_text(value).map(Self)
140 }
141
142 #[must_use]
144 pub fn as_str(&self) -> &str {
145 &self.0
146 }
147
148 #[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#[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 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
255pub enum ComponentKindParseError {
256 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}