use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use thiserror::Error;
pub mod names {
pub const ENDPOINT: super::EventName = super::EventName::const_new("endpoint");
pub const MESSAGE: super::EventName = super::EventName::const_new("message");
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EventName(Cow<'static, str>);
impl EventName {
pub(crate) const fn const_new(value: &'static str) -> Self {
Self(Cow::Borrowed(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn is(&self, name: &Self) -> bool {
self.0 == name.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum EventNameError {
#[error("an event name may not be empty")]
Empty,
#[error("an event name may not contain a line terminator")]
LineTerminator,
}
impl FromStr for EventName {
type Err = EventNameError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.is_empty() {
return Err(EventNameError::Empty);
}
if value.contains(['\n', '\r']) {
return Err(EventNameError::LineTerminator);
}
Ok(Self(Cow::Owned(value.to_string())))
}
}
impl fmt::Display for EventName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::{EventName, EventNameError, names};
use core::str::FromStr;
#[derive(Debug)]
enum Failure {
Complaint(String),
Refused(EventNameError),
}
impl From<EventNameError> for Failure {
fn from(error: EventNameError) -> Self {
Self::Refused(error)
}
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Complaint(text) => f.write_str(text),
Self::Refused(error) => write!(f, "refused when a name was wanted: {error}"),
}
}
}
type TestError = Failure;
fn fail(message: impl Into<String>) -> TestError {
Failure::Complaint(message.into())
}
fn parses_to(value: &str, expected: &str) -> Result<(), TestError> {
let name = EventName::from_str(value)?;
if name.as_str() != expected {
return Err(fail(format!(
"{value:?} parsed as {:?}, not {expected:?}",
name.as_str()
)));
}
Ok(())
}
fn refused_with(value: &str, expected: &EventNameError) -> Result<(), TestError> {
match EventName::from_str(value) {
Err(error) if &error == expected => Ok(()),
Err(error) => Err(fail(format!(
"{value:?} was refused as {error:?}, not {expected:?}"
))),
Ok(name) => Err(fail(format!(
"{value:?} parsed as {:?}, which cannot be encoded safely",
name.as_str()
))),
}
}
#[test]
fn a_plain_name_parses() -> Result<(), TestError> {
parses_to("endpoint", "endpoint")
}
#[test]
fn a_colon_is_legal_because_the_value_is_the_rest_of_the_line() -> Result<(), TestError> {
parses_to("a:b", "a:b")
}
#[test]
fn a_newline_in_a_name_is_refused() -> Result<(), TestError> {
refused_with(
"message\ndata: {\"jsonrpc\":\"2.0\"}",
&EventNameError::LineTerminator,
)
}
#[test]
fn a_carriage_return_in_a_name_is_refused() -> Result<(), TestError> {
refused_with("message\rdata: x", &EventNameError::LineTerminator)
}
#[test]
fn an_empty_name_is_refused() -> Result<(), TestError> {
refused_with("", &EventNameError::Empty)
}
#[test]
fn the_legacy_names_are_the_specified_spellings() -> Result<(), TestError> {
if names::ENDPOINT.as_str() != "endpoint" {
return Err(fail("the endpoint constant is not spelled `endpoint`"));
}
if names::MESSAGE.as_str() != "message" {
return Err(fail("the message constant is not spelled `message`"));
}
let parsed = EventName::from_str("message")?;
if !parsed.is(&names::MESSAGE) {
return Err(fail("a parsed name does not match the message constant"));
}
if names::MESSAGE.is(&names::ENDPOINT) {
return Err(fail("the two legacy names are equal"));
}
Ok(())
}
}