#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_component::ReferenceDesignator;
use use_pin::{PinIdentifier, PinRef};
pub mod prelude {
pub use crate::{
CircuitId, CircuitName, CircuitTextError, Connection, ConnectionTarget, NetId, NodeId,
Terminal,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CircuitTextError {
Empty,
}
impl fmt::Display for CircuitTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("circuit text cannot be empty"),
}
}
}
impl Error for CircuitTextError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CircuitId(String);
impl CircuitId {
pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CircuitId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for CircuitId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for CircuitId {
type Err = CircuitTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct CircuitName(String);
impl CircuitName {
pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CircuitName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for CircuitName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for CircuitName {
type Err = CircuitTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NodeId(String);
impl NodeId {
pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for NodeId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for NodeId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for NodeId {
type Err = CircuitTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct NetId(String);
impl NetId {
pub fn new(value: impl AsRef<str>) -> Result<Self, CircuitTextError> {
non_empty_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for NetId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for NetId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for NetId {
type Err = CircuitTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Terminal {
pin_ref: PinRef,
}
impl Terminal {
#[must_use]
pub const fn from_pin_ref(pin_ref: PinRef) -> Self {
Self { pin_ref }
}
#[must_use]
pub const fn new(component: ReferenceDesignator, pin: PinIdentifier) -> Self {
Self::from_pin_ref(PinRef::new(component, pin))
}
#[must_use]
pub const fn pin_ref(&self) -> &PinRef {
&self.pin_ref
}
}
impl fmt::Display for Terminal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.pin_ref.fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ConnectionTarget {
Net(NetId),
Node(NodeId),
}
impl fmt::Display for ConnectionTarget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Net(net) => write!(formatter, "net:{net}"),
Self::Node(node) => write!(formatter, "node:{node}"),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Connection {
terminal: Terminal,
target: ConnectionTarget,
}
impl Connection {
#[must_use]
pub const fn new(terminal: Terminal, target: ConnectionTarget) -> Self {
Self { terminal, target }
}
#[must_use]
pub const fn to_net(terminal: Terminal, net: NetId) -> Self {
Self::new(terminal, ConnectionTarget::Net(net))
}
#[must_use]
pub const fn to_node(terminal: Terminal, node: NodeId) -> Self {
Self::new(terminal, ConnectionTarget::Node(node))
}
#[must_use]
pub const fn terminal(&self) -> &Terminal {
&self.terminal
}
#[must_use]
pub const fn target(&self) -> &ConnectionTarget {
&self.target
}
}
fn non_empty_text(value: impl AsRef<str>) -> Result<String, CircuitTextError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(CircuitTextError::Empty)
} else {
Ok(trimmed.to_string())
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::{CircuitName, CircuitTextError, Connection, NetId, Terminal};
use use_component::ReferenceDesignator;
use use_pin::{PinNumber, PinRef};
#[test]
fn accepts_valid_circuit_names() -> Result<(), CircuitTextError> {
let name = CircuitName::new("input filter")?;
assert_eq!(name.as_str(), "input filter");
Ok(())
}
#[test]
fn rejects_empty_circuit_names() {
assert_eq!(CircuitName::new(" "), Err(CircuitTextError::Empty));
}
#[test]
fn creates_terminals() -> Result<(), Box<dyn std::error::Error>> {
let pin = PinRef::numbered(ReferenceDesignator::new("R1")?, PinNumber::new(1)?);
let terminal = Terminal::from_pin_ref(pin);
assert_eq!(terminal.to_string(), "R1:1");
Ok(())
}
#[test]
fn creates_connections() -> Result<(), Box<dyn std::error::Error>> {
let pin = PinRef::numbered(ReferenceDesignator::new("R1")?, PinNumber::new(1)?);
let connection = Connection::to_net(Terminal::from_pin_ref(pin), NetId::new("SENSE")?);
assert_eq!(connection.target().to_string(), "net:SENSE");
Ok(())
}
#[test]
fn sorts_connections_deterministically() -> Result<(), Box<dyn std::error::Error>> {
let connections = BTreeSet::from([
Connection::to_net(
Terminal::from_pin_ref(PinRef::numbered(
ReferenceDesignator::new("R2")?,
PinNumber::new(1)?,
)),
NetId::new("B")?,
),
Connection::to_net(
Terminal::from_pin_ref(PinRef::numbered(
ReferenceDesignator::new("R1")?,
PinNumber::new(1)?,
)),
NetId::new("A")?,
),
]);
let ordered: Vec<_> = connections
.iter()
.map(|connection| connection.terminal().to_string())
.collect();
assert_eq!(ordered, vec!["R1:1", "R2:1"]);
Ok(())
}
}