Skip to main content

use_circuit/
lib.rs

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
10/// Commonly used circuit primitives.
11pub mod prelude {
12    pub use crate::{
13        CircuitId, CircuitName, CircuitTextError, Connection, ConnectionTarget, NetId, NodeId,
14        Terminal,
15    };
16}
17
18/// Errors returned by non-empty circuit text wrappers.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub enum CircuitTextError {
21    /// The text was empty after trimming whitespace.
22    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/// A stable circuit identifier.
36#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
37pub struct CircuitId(String);
38
39impl CircuitId {
40    /// Creates a circuit ID from non-empty text.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`CircuitTextError::Empty`] when the trimmed value is empty.
45    pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
46        non_empty_text(value).map(Self)
47    }
48
49    /// Returns the ID text.
50    #[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/// A human-readable circuit name.
77#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
78pub struct CircuitName(String);
79
80impl CircuitName {
81    /// Creates a circuit name from non-empty text.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`CircuitTextError::Empty`] when the trimmed value is empty.
86    pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
87        non_empty_text(value).map(Self)
88    }
89
90    /// Returns the name text.
91    #[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/// A circuit node identifier.
118#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
119pub struct NodeId(String);
120
121impl NodeId {
122    /// Creates a node ID from non-empty text.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`CircuitTextError::Empty`] when the trimmed value is empty.
127    pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
128        non_empty_text(value).map(Self)
129    }
130
131    /// Returns the node ID text.
132    #[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/// A net identifier.
159#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
160pub struct NetId(String);
161
162impl NetId {
163    /// Creates a net ID from non-empty text.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`CircuitTextError::Empty`] when the trimmed value is empty.
168    pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
169        non_empty_text(value).map(Self)
170    }
171
172    /// Returns the net ID text.
173    #[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/// A component terminal represented as a component pin reference.
200#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
201pub struct Terminal {
202    pin_ref: PinRef,
203}
204
205impl Terminal {
206    /// Creates a terminal from a pin reference.
207    #[must_use]
208    pub const fn from_pin_ref(pin_ref: PinRef) -> Self {
209        Self { pin_ref }
210    }
211
212    /// Creates a terminal from a component and pin identifier.
213    #[must_use]
214    pub const fn new(component: ReferenceDesignator, pin: PinIdentifier) -> Self {
215        Self::from_pin_ref(PinRef::new(component, pin))
216    }
217
218    /// Returns the underlying pin reference.
219    #[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/// The graph target for a terminal connection.
232#[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/// A descriptive connection from a terminal to a net or node.
248#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
249pub struct Connection {
250    terminal: Terminal,
251    target: ConnectionTarget,
252}
253
254impl Connection {
255    /// Creates a connection from a terminal to a target.
256    #[must_use]
257    pub const fn new(terminal: Terminal, target: ConnectionTarget) -> Self {
258        Self { terminal, target }
259    }
260
261    /// Creates a terminal-to-net connection.
262    #[must_use]
263    pub const fn to_net(terminal: Terminal, net: NetId) -> Self {
264        Self::new(terminal, ConnectionTarget::Net(net))
265    }
266
267    /// Creates a terminal-to-node connection.
268    #[must_use]
269    pub const fn to_node(terminal: Terminal, node: NodeId) -> Self {
270        Self::new(terminal, ConnectionTarget::Node(node))
271    }
272
273    /// Returns the connected terminal.
274    #[must_use]
275    pub const fn terminal(&self) -> &Terminal {
276        &self.terminal
277    }
278
279    /// Returns the connection target.
280    #[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}