#![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 RobotName(String);
impl RobotName {
pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
non_empty_robot_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 RobotName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RobotName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RobotName {
type Err = RobotTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum RobotKind {
Arm,
Mobile,
Humanoid,
Drone,
Quadruped,
Manipulator,
Collaborative,
Industrial,
Educational,
Unknown,
Custom(String),
}
impl fmt::Display for RobotKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Arm => "arm",
Self::Mobile => "mobile",
Self::Humanoid => "humanoid",
Self::Drone => "drone",
Self::Quadruped => "quadruped",
Self::Manipulator => "manipulator",
Self::Collaborative => "collaborative",
Self::Industrial => "industrial",
Self::Educational => "educational",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for RobotKind {
type Err = RobotKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(RobotKindParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"arm" | "robot-arm" => Ok(Self::Arm),
"mobile" | "mobile-robot" => Ok(Self::Mobile),
"humanoid" => Ok(Self::Humanoid),
"drone" | "uav" => Ok(Self::Drone),
"quadruped" => Ok(Self::Quadruped),
"manipulator" => Ok(Self::Manipulator),
"collaborative" | "cobot" => Ok(Self::Collaborative),
"industrial" => Ok(Self::Industrial),
"educational" => Ok(Self::Educational),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RobotModel(String);
impl RobotModel {
pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
non_empty_robot_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 RobotModel {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RobotModel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RobotModel {
type Err = RobotTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RobotManufacturer(String);
impl RobotManufacturer {
pub fn new(value: impl AsRef<str>) -> Result<Self, RobotTextError> {
non_empty_robot_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 RobotManufacturer {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for RobotManufacturer {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for RobotManufacturer {
type Err = RobotTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RobotTextError {
Empty,
}
impl fmt::Display for RobotTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("robot text cannot be empty"),
}
}
}
impl Error for RobotTextError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RobotKindParseError {
Empty,
}
impl fmt::Display for RobotKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("robot kind cannot be empty"),
}
}
}
impl Error for RobotKindParseError {}
fn non_empty_robot_text(value: impl AsRef<str>) -> Result<String, RobotTextError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(RobotTextError::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::{RobotKind, RobotKindParseError, RobotModel, RobotName, RobotTextError};
#[test]
fn constructs_valid_robot_name() -> Result<(), RobotTextError> {
let name = RobotName::new(" Atlas ")?;
assert_eq!(name.as_str(), "Atlas");
assert_eq!(name.to_string(), "Atlas");
Ok(())
}
#[test]
fn rejects_empty_robot_name() {
assert_eq!(RobotName::new(" "), Err(RobotTextError::Empty));
}
#[test]
fn displays_and_parses_robot_kind() -> Result<(), RobotKindParseError> {
assert_eq!("robot arm".parse::<RobotKind>()?, RobotKind::Arm);
assert_eq!(RobotKind::Collaborative.to_string(), "collaborative");
Ok(())
}
#[test]
fn stores_custom_robot_kind() -> Result<(), RobotKindParseError> {
assert_eq!(
"pipe-crawler".parse::<RobotKind>()?,
RobotKind::Custom("pipe-crawler".to_string())
);
Ok(())
}
#[test]
fn constructs_robot_model() -> Result<(), RobotTextError> {
let model = RobotModel::new("RX-4")?;
assert_eq!(model.as_str(), "RX-4");
Ok(())
}
}