use std::fmt;
use std::str::FromStr;
use crate::error::{Result, SparkplugError};
pub const NAMESPACE: &str = "spBv1.0";
pub const STATE_TOKEN: &str = "STATE";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum MessageType {
NBirth,
NDeath,
NData,
NCmd,
DBirth,
DDeath,
DData,
DCmd,
State,
}
impl MessageType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::NBirth => "NBIRTH",
Self::NDeath => "NDEATH",
Self::NData => "NDATA",
Self::NCmd => "NCMD",
Self::DBirth => "DBIRTH",
Self::DDeath => "DDEATH",
Self::DData => "DDATA",
Self::DCmd => "DCMD",
Self::State => "STATE",
}
}
#[must_use]
pub const fn has_device(self) -> bool {
matches!(self, Self::DBirth | Self::DDeath | Self::DData | Self::DCmd)
}
#[must_use]
pub const fn carries_seq(self) -> bool {
matches!(
self,
Self::NBirth | Self::NData | Self::DBirth | Self::DData | Self::DDeath
)
}
#[must_use]
pub const fn is_command(self) -> bool {
matches!(self, Self::NCmd | Self::DCmd)
}
}
impl fmt::Display for MessageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for MessageType {
type Err = SparkplugError;
fn from_str(s: &str) -> Result<Self> {
let ty = match s {
"NBIRTH" => Self::NBirth,
"NDEATH" => Self::NDeath,
"NDATA" => Self::NData,
"NCMD" => Self::NCmd,
"DBIRTH" => Self::DBirth,
"DDEATH" => Self::DDeath,
"DDATA" => Self::DData,
"DCMD" => Self::DCmd,
"STATE" => Self::State,
other => {
return Err(SparkplugError::InvalidTopic(format!(
"unknown message type {other:?}"
)));
}
};
Ok(ty)
}
}
fn validate_id(s: &str) -> Result<()> {
if s.is_empty() {
return Err(SparkplugError::InvalidId("identifier is empty".to_owned()));
}
if s.contains(['+', '/', '#']) {
return Err(SparkplugError::InvalidId(format!(
"{s:?} contains a reserved character (+, /, or #)"
)));
}
Ok(())
}
macro_rules! id_newtype {
($(#[$m:meta])* $name:ident) => {
$(#[$m])*
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct $name(String);
impl $name {
pub fn new(s: impl Into<String>) -> Result<Self> {
let s = s.into();
validate_id(&s)?;
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
};
}
id_newtype!(
GroupId
);
id_newtype!(
EdgeNodeId
);
id_newtype!(
DeviceId
);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SparkplugTopic {
Node {
group: GroupId,
edge: EdgeNodeId,
ty: MessageType,
},
Device {
group: GroupId,
edge: EdgeNodeId,
device: DeviceId,
ty: MessageType,
},
HostState {
host_id: String,
},
}
impl SparkplugTopic {
pub fn node(group: GroupId, edge: EdgeNodeId, ty: MessageType) -> Result<Self> {
if ty.has_device() || ty == MessageType::State {
return Err(SparkplugError::InvalidTopic(format!(
"{ty} is not an Edge-Node message type"
)));
}
Ok(Self::Node { group, edge, ty })
}
pub fn device(
group: GroupId,
edge: EdgeNodeId,
device: DeviceId,
ty: MessageType,
) -> Result<Self> {
if !ty.has_device() {
return Err(SparkplugError::InvalidTopic(format!(
"{ty} is not a Device message type"
)));
}
Ok(Self::Device {
group,
edge,
device,
ty,
})
}
#[must_use]
pub fn message_type(&self) -> MessageType {
match self {
Self::Node { ty, .. } | Self::Device { ty, .. } => *ty,
Self::HostState { .. } => MessageType::State,
}
}
pub fn parse(topic: &str) -> Result<Self> {
let parts: Vec<&str> = topic.split('/').collect();
if parts.first() != Some(&NAMESPACE) {
return Err(SparkplugError::InvalidTopic(format!(
"topic must start with {NAMESPACE:?}: {topic:?}"
)));
}
match parts.as_slice() {
[_, token, host] if *token == STATE_TOKEN => {
validate_id(host)?;
Ok(Self::HostState {
host_id: (*host).to_owned(),
})
}
[_, group, ty_token, edge] => {
let ty = MessageType::from_str(ty_token)?;
if ty.has_device() || ty == MessageType::State {
return Err(SparkplugError::InvalidTopic(format!(
"{ty} requires a different token count"
)));
}
Self::node(GroupId::new(*group)?, EdgeNodeId::new(*edge)?, ty)
}
[_, group, ty_token, edge, device] => {
let ty = MessageType::from_str(ty_token)?;
if !ty.has_device() {
return Err(SparkplugError::InvalidTopic(format!(
"{ty} must not include a device_id"
)));
}
Self::device(
GroupId::new(*group)?,
EdgeNodeId::new(*edge)?,
DeviceId::new(*device)?,
ty,
)
}
_ => Err(SparkplugError::InvalidTopic(format!(
"unexpected token count in topic {topic:?}"
))),
}
}
}
impl fmt::Display for SparkplugTopic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Node { group, edge, ty } => {
write!(f, "{NAMESPACE}/{group}/{ty}/{edge}")
}
Self::Device {
group,
edge,
device,
ty,
} => write!(f, "{NAMESPACE}/{group}/{ty}/{edge}/{device}"),
Self::HostState { host_id } => write!(f, "{NAMESPACE}/{STATE_TOKEN}/{host_id}"),
}
}
}