1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7use use_component::ReferenceDesignator;
8use use_pin::{PinIdentifier, PinRef};
9
10pub mod prelude {
12 pub use crate::{
13 CircuitId, CircuitName, CircuitTextError, Connection, ConnectionTarget, NetId, NodeId,
14 Terminal,
15 };
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum CircuitTextError {
21 Empty,
23}
24
25impl fmt::Display for CircuitTextError {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::Empty => formatter.write_str("circuit text cannot be empty"),
29 }
30 }
31}
32
33impl Error for CircuitTextError {}
34
35#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct CircuitId(String);
38
39impl CircuitId {
40 pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
46 non_empty_text(value).map(Self)
47 }
48
49 #[must_use]
51 pub fn as_str(&self) -> &str {
52 &self.0
53 }
54}
55
56impl AsRef<str> for CircuitId {
57 fn as_ref(&self) -> &str {
58 self.as_str()
59 }
60}
61
62impl fmt::Display for CircuitId {
63 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64 formatter.write_str(self.as_str())
65 }
66}
67
68impl FromStr for CircuitId {
69 type Err = CircuitTextError;
70
71 fn from_str(value: &str) -> Result<Self, Self::Err> {
72 Self::new(value)
73 }
74}
75
76#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
78pub struct CircuitName(String);
79
80impl CircuitName {
81 pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
87 non_empty_text(value).map(Self)
88 }
89
90 #[must_use]
92 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95}
96
97impl AsRef<str> for CircuitName {
98 fn as_ref(&self) -> &str {
99 self.as_str()
100 }
101}
102
103impl fmt::Display for CircuitName {
104 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
105 formatter.write_str(self.as_str())
106 }
107}
108
109impl FromStr for CircuitName {
110 type Err = CircuitTextError;
111
112 fn from_str(value: &str) -> Result<Self, Self::Err> {
113 Self::new(value)
114 }
115}
116
117#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119pub struct NodeId(String);
120
121impl NodeId {
122 pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
128 non_empty_text(value).map(Self)
129 }
130
131 #[must_use]
133 pub fn as_str(&self) -> &str {
134 &self.0
135 }
136}
137
138impl AsRef<str> for NodeId {
139 fn as_ref(&self) -> &str {
140 self.as_str()
141 }
142}
143
144impl fmt::Display for NodeId {
145 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
146 formatter.write_str(self.as_str())
147 }
148}
149
150impl FromStr for NodeId {
151 type Err = CircuitTextError;
152
153 fn from_str(value: &str) -> Result<Self, Self::Err> {
154 Self::new(value)
155 }
156}
157
158#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
160pub struct NetId(String);
161
162impl NetId {
163 pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
169 non_empty_text(value).map(Self)
170 }
171
172 #[must_use]
174 pub fn as_str(&self) -> &str {
175 &self.0
176 }
177}
178
179impl AsRef<str> for NetId {
180 fn as_ref(&self) -> &str {
181 self.as_str()
182 }
183}
184
185impl fmt::Display for NetId {
186 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187 formatter.write_str(self.as_str())
188 }
189}
190
191impl FromStr for NetId {
192 type Err = CircuitTextError;
193
194 fn from_str(value: &str) -> Result<Self, Self::Err> {
195 Self::new(value)
196 }
197}
198
199#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct Terminal {
202 pin_ref: PinRef,
203}
204
205impl Terminal {
206 #[must_use]
208 pub const fn from_pin_ref(pin_ref: PinRef) -> Self {
209 Self { pin_ref }
210 }
211
212 #[must_use]
214 pub const fn new(component: ReferenceDesignator, pin: PinIdentifier) -> Self {
215 Self::from_pin_ref(PinRef::new(component, pin))
216 }
217
218 #[must_use]
220 pub const fn pin_ref(&self) -> &PinRef {
221 &self.pin_ref
222 }
223}
224
225impl fmt::Display for Terminal {
226 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227 self.pin_ref.fmt(formatter)
228 }
229}
230
231#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
233pub enum ConnectionTarget {
234 Net(NetId),
235 Node(NodeId),
236}
237
238impl fmt::Display for ConnectionTarget {
239 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
240 match self {
241 Self::Net(net) => write!(formatter, "net:{net}"),
242 Self::Node(node) => write!(formatter, "node:{node}"),
243 }
244 }
245}
246
247#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
249pub struct Connection {
250 terminal: Terminal,
251 target: ConnectionTarget,
252}
253
254impl Connection {
255 #[must_use]
257 pub const fn new(terminal: Terminal, target: ConnectionTarget) -> Self {
258 Self { terminal, target }
259 }
260
261 #[must_use]
263 pub const fn to_net(terminal: Terminal, net: NetId) -> Self {
264 Self::new(terminal, ConnectionTarget::Net(net))
265 }
266
267 #[must_use]
269 pub const fn to_node(terminal: Terminal, node: NodeId) -> Self {
270 Self::new(terminal, ConnectionTarget::Node(node))
271 }
272
273 #[must_use]
275 pub const fn terminal(&self) -> &Terminal {
276 &self.terminal
277 }
278
279 #[must_use]
281 pub const fn target(&self) -> &ConnectionTarget {
282 &self.target
283 }
284}
285
286fn non_empty_text(value: impl AsRef<str>) -> Result<String, CircuitTextError> {
287 let trimmed = value.as_ref().trim();
288 if trimmed.is_empty() {
289 Err(CircuitTextError::Empty)
290 } else {
291 Ok(trimmed.to_string())
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use std::collections::BTreeSet;
298
299 use super::{CircuitName, CircuitTextError, Connection, NetId, Terminal};
300 use use_component::ReferenceDesignator;
301 use use_pin::{PinNumber, PinRef};
302
303 #[test]
304 fn accepts_valid_circuit_names() -> Result<(), CircuitTextError> {
305 let name = CircuitName::new("input filter")?;
306
307 assert_eq!(name.as_str(), "input filter");
308 Ok(())
309 }
310
311 #[test]
312 fn rejects_empty_circuit_names() {
313 assert_eq!(CircuitName::new(" "), Err(CircuitTextError::Empty));
314 }
315
316 #[test]
317 fn creates_terminals() -> Result<(), Box<dyn std::error::Error>> {
318 let pin = PinRef::numbered(ReferenceDesignator::new("R1")?, PinNumber::new(1)?);
319 let terminal = Terminal::from_pin_ref(pin);
320
321 assert_eq!(terminal.to_string(), "R1:1");
322 Ok(())
323 }
324
325 #[test]
326 fn creates_connections() -> Result<(), Box<dyn std::error::Error>> {
327 let pin = PinRef::numbered(ReferenceDesignator::new("R1")?, PinNumber::new(1)?);
328 let connection = Connection::to_net(Terminal::from_pin_ref(pin), NetId::new("SENSE")?);
329
330 assert_eq!(connection.target().to_string(), "net:SENSE");
331 Ok(())
332 }
333
334 #[test]
335 fn sorts_connections_deterministically() -> Result<(), Box<dyn std::error::Error>> {
336 let connections = BTreeSet::from([
337 Connection::to_net(
338 Terminal::from_pin_ref(PinRef::numbered(
339 ReferenceDesignator::new("R2")?,
340 PinNumber::new(1)?,
341 )),
342 NetId::new("B")?,
343 ),
344 Connection::to_net(
345 Terminal::from_pin_ref(PinRef::numbered(
346 ReferenceDesignator::new("R1")?,
347 PinNumber::new(1)?,
348 )),
349 NetId::new("A")?,
350 ),
351 ]);
352 let ordered: Vec<_> = connections
353 .iter()
354 .map(|connection| connection.terminal().to_string())
355 .collect();
356
357 assert_eq!(ordered, vec!["R1:1", "R2:1"]);
358 Ok(())
359 }
360}