#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ActuatorName(String);
impl ActuatorName {
pub fn new(value: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
non_empty_actuator_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 ActuatorName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for ActuatorName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for ActuatorName {
type Err = ActuatorTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ActuatorKind {
ElectricMotor,
Servo,
StepperMotor,
LinearActuator,
Hydraulic,
Pneumatic,
Solenoid,
Unknown,
Custom(String),
}
impl fmt::Display for ActuatorKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::ElectricMotor => "electric-motor",
Self::Servo => "servo",
Self::StepperMotor => "stepper-motor",
Self::LinearActuator => "linear-actuator",
Self::Hydraulic => "hydraulic",
Self::Pneumatic => "pneumatic",
Self::Solenoid => "solenoid",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for ActuatorKind {
type Err = ActuatorKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ActuatorKindParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"electric-motor" | "motor" => Ok(Self::ElectricMotor),
"servo" => Ok(Self::Servo),
"stepper-motor" | "stepper" => Ok(Self::StepperMotor),
"linear-actuator" => Ok(Self::LinearActuator),
"hydraulic" => Ok(Self::Hydraulic),
"pneumatic" => Ok(Self::Pneumatic),
"solenoid" => Ok(Self::Solenoid),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ActuatorState {
Idle,
Enabled,
Disabled,
Moving,
Faulted,
Unknown,
Custom(String),
}
impl fmt::Display for ActuatorState {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Idle => "idle",
Self::Enabled => "enabled",
Self::Disabled => "disabled",
Self::Moving => "moving",
Self::Faulted => "faulted",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for ActuatorState {
type Err = ActuatorStateParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ActuatorStateParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"idle" => Ok(Self::Idle),
"enabled" | "enable" => Ok(Self::Enabled),
"disabled" | "disable" => Ok(Self::Disabled),
"moving" => Ok(Self::Moving),
"faulted" | "fault" => Ok(Self::Faulted),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ActuatorRating {
label: String,
value: Option<f64>,
unit: Option<String>,
}
impl ActuatorRating {
pub fn new(label: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
Ok(Self {
label: non_empty_actuator_text(label)?,
value: None,
unit: None,
})
}
pub fn with_value(mut self, value: f64) -> Result<Self, ActuatorRatingError> {
if !value.is_finite() {
return Err(ActuatorRatingError::NonFinite);
}
self.value = Some(value);
Ok(self)
}
pub fn with_unit(mut self, unit: impl AsRef<str>) -> Result<Self, ActuatorTextError> {
self.unit = Some(non_empty_actuator_text(unit)?);
Ok(self)
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub const fn value(&self) -> Option<f64> {
self.value
}
#[must_use]
pub fn unit(&self) -> Option<&str> {
self.unit.as_deref()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ActuatorTextError {
Empty,
}
impl fmt::Display for ActuatorTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("actuator text cannot be empty"),
}
}
}
impl Error for ActuatorTextError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ActuatorKindParseError {
Empty,
}
impl fmt::Display for ActuatorKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("actuator kind cannot be empty"),
}
}
}
impl Error for ActuatorKindParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ActuatorStateParseError {
Empty,
}
impl fmt::Display for ActuatorStateParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("actuator state cannot be empty"),
}
}
}
impl Error for ActuatorStateParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ActuatorRatingError {
NonFinite,
}
impl fmt::Display for ActuatorRatingError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonFinite => formatter.write_str("actuator rating value must be finite"),
}
}
}
impl Error for ActuatorRatingError {}
fn non_empty_actuator_text(value: impl AsRef<str>) -> Result<String, ActuatorTextError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(ActuatorTextError::Empty)
} else {
Ok(trimmed.to_string())
}
}
fn normalized_token(value: &str) -> String {
value
.trim()
.chars()
.map(|character| match character {
'_' | ' ' => '-',
other => other.to_ascii_lowercase(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{
ActuatorKind, ActuatorKindParseError, ActuatorName, ActuatorRating, ActuatorState,
ActuatorStateParseError, ActuatorTextError,
};
#[test]
fn constructs_valid_actuator_name() -> Result<(), ActuatorTextError> {
let name = ActuatorName::new(" shoulder-servo ")?;
assert_eq!(name.as_str(), "shoulder-servo");
Ok(())
}
#[test]
fn rejects_empty_actuator_name() {
assert_eq!(ActuatorName::new(""), Err(ActuatorTextError::Empty));
}
#[test]
fn displays_and_parses_actuator_kind() -> Result<(), ActuatorKindParseError> {
assert_eq!(
"stepper motor".parse::<ActuatorKind>()?,
ActuatorKind::StepperMotor
);
assert_eq!(ActuatorKind::LinearActuator.to_string(), "linear-actuator");
Ok(())
}
#[test]
fn displays_and_parses_actuator_state() -> Result<(), ActuatorStateParseError> {
assert_eq!("enabled".parse::<ActuatorState>()?, ActuatorState::Enabled);
assert_eq!(ActuatorState::Faulted.to_string(), "faulted");
Ok(())
}
#[test]
fn stores_custom_actuator_kind() -> Result<(), ActuatorKindParseError> {
assert_eq!(
"shape-memory".parse::<ActuatorKind>()?,
ActuatorKind::Custom("shape-memory".to_string())
);
Ok(())
}
#[test]
fn constructs_descriptive_rating() -> Result<(), Box<dyn std::error::Error>> {
let rating = ActuatorRating::new("rated torque")?
.with_value(12.0)?
.with_unit("N m")?;
assert_eq!(rating.label(), "rated torque");
assert_eq!(rating.value(), Some(12.0));
assert_eq!(rating.unit(), Some("N m"));
Ok(())
}
}