#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
pub mod prelude {
pub use crate::{
ComponentId, ComponentKind, ComponentKindParseError, ComponentTextError, ComponentValue,
ReferenceDesignator,
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ComponentTextError {
Empty,
}
impl fmt::Display for ComponentTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("component text cannot be empty"),
}
}
}
impl Error for ComponentTextError {}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ComponentId(String);
impl ComponentId {
pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
non_empty_component_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for ComponentId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ComponentId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for ComponentId {
type Err = ComponentTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ReferenceDesignator(String);
impl ReferenceDesignator {
pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
non_empty_component_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for ReferenceDesignator {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ReferenceDesignator {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for ReferenceDesignator {
type Err = ComponentTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ComponentValue(String);
impl ComponentValue {
pub fn new(value: impl AsRef<str>) -> Result<Self, ComponentTextError> {
non_empty_component_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for ComponentValue {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ComponentValue {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for ComponentValue {
type Err = ComponentTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ComponentKind {
Resistor,
Capacitor,
Inductor,
Diode,
Transistor,
IntegratedCircuit,
Connector,
Switch,
Sensor,
PowerSupply,
Unknown,
Custom(String),
}
impl ComponentKind {
pub fn custom(value: impl AsRef<str>) -> Result<Self, ComponentKindParseError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(ComponentKindParseError::Empty)
} else {
Ok(Self::Custom(trimmed.to_string()))
}
}
}
impl fmt::Display for ComponentKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Resistor => "resistor",
Self::Capacitor => "capacitor",
Self::Inductor => "inductor",
Self::Diode => "diode",
Self::Transistor => "transistor",
Self::IntegratedCircuit => "integrated-circuit",
Self::Connector => "connector",
Self::Switch => "switch",
Self::Sensor => "sensor",
Self::PowerSupply => "power-supply",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for ComponentKind {
type Err = ComponentKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ComponentKindParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"resistor" => Ok(Self::Resistor),
"capacitor" => Ok(Self::Capacitor),
"inductor" => Ok(Self::Inductor),
"diode" => Ok(Self::Diode),
"transistor" => Ok(Self::Transistor),
"integrated-circuit" | "ic" => Ok(Self::IntegratedCircuit),
"connector" => Ok(Self::Connector),
"switch" => Ok(Self::Switch),
"sensor" => Ok(Self::Sensor),
"power-supply" => Ok(Self::PowerSupply),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ComponentKindParseError {
Empty,
}
impl fmt::Display for ComponentKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("component kind cannot be empty"),
}
}
}
impl Error for ComponentKindParseError {}
fn non_empty_component_text(value: impl AsRef<str>) -> Result<String, ComponentTextError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(ComponentTextError::Empty)
} else {
Ok(trimmed.to_string())
}
}
fn normalized_token(value: &str) -> String {
value.trim().to_ascii_lowercase().replace(['_', ' '], "-")
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use super::{ComponentKind, ComponentKindParseError, ComponentTextError, ReferenceDesignator};
#[test]
fn accepts_valid_reference_designators() -> Result<(), ComponentTextError> {
let reference = ReferenceDesignator::new("R1")?;
assert_eq!(reference.as_str(), "R1");
assert_eq!(reference.to_string(), "R1");
Ok(())
}
#[test]
fn rejects_empty_reference_designators() {
assert_eq!(
ReferenceDesignator::new(" "),
Err(ComponentTextError::Empty)
);
}
#[test]
fn displays_and_parses_component_kinds() -> Result<(), ComponentKindParseError> {
assert_eq!(
"resistor".parse::<ComponentKind>()?,
ComponentKind::Resistor
);
assert_eq!(
"Integrated Circuit".parse::<ComponentKind>()?,
ComponentKind::IntegratedCircuit
);
assert_eq!(ComponentKind::PowerSupply.to_string(), "power-supply");
Ok(())
}
#[test]
fn supports_custom_component_kinds() -> Result<(), ComponentKindParseError> {
let kind = ComponentKind::custom("fuse")?;
assert_eq!(kind, ComponentKind::Custom("fuse".to_string()));
assert_eq!("relay".parse::<ComponentKind>()?.to_string(), "relay");
Ok(())
}
#[test]
fn sorts_reference_designators_deterministically() -> Result<(), ComponentTextError> {
let references = BTreeSet::from([
ReferenceDesignator::new("R2")?,
ReferenceDesignator::new("R1")?,
ReferenceDesignator::new("C1")?,
]);
let ordered: Vec<_> = references.iter().map(ReferenceDesignator::as_str).collect();
assert_eq!(ordered, vec!["C1", "R1", "R2"]);
Ok(())
}
}