use std::fmt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
const RESERVED_PREFIX: &str = "arf.";
pub(super) const MAX_TOPIC_BYTES: usize = 128;
const ENVELOPE_OVERHEAD_BYTES: usize = 128;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TopicError {
Empty,
TooLong {
len: usize,
max: usize,
},
EmptySegment,
InvalidCharacter(char),
Reserved,
}
impl fmt::Display for TopicError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "topic is empty"),
Self::TooLong { len, max } => {
write!(f, "topic is {len} bytes long, the maximum is {max}")
}
Self::EmptySegment => write!(f, "topic contains an empty segment"),
Self::InvalidCharacter(c) => write!(f, "topic contains invalid character {c:?}"),
Self::Reserved => write!(f, "the arf. topic namespace is reserved"),
}
}
}
impl std::error::Error for TopicError {}
fn validate_name(name: &str) -> Result<(), TopicError> {
if name.is_empty() {
return Err(TopicError::Empty);
}
if name.len() > MAX_TOPIC_BYTES {
return Err(TopicError::TooLong {
len: name.len(),
max: MAX_TOPIC_BYTES,
});
}
for segment in name.split('.') {
if segment.is_empty() {
return Err(TopicError::EmptySegment);
}
if let Some(c) = segment
.chars()
.find(|c| !(c.is_ascii_alphanumeric() || *c == '_' || *c == '-'))
{
return Err(TopicError::InvalidCharacter(c));
}
}
Ok(())
}
fn is_reserved_name(name: &str) -> bool {
name.starts_with(RESERVED_PREFIX)
}
pub(super) fn validate_prefix(prefix: &str) -> Result<(), TopicError> {
validate_name(prefix)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct Topic(String);
impl Topic {
pub fn internal(name: &str) -> Result<Self, TopicError> {
validate_name(name)?;
Ok(Self(name.to_owned()))
}
pub fn external(name: &str) -> Result<Self, TopicError> {
validate_name(name)?;
if is_reserved_name(name) {
return Err(TopicError::Reserved);
}
Ok(Self(name.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "only the tests ask a topic whether it is reserved"
)
)]
pub fn is_reserved(&self) -> bool {
is_reserved_name(&self.0)
}
}
impl fmt::Display for Topic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ExecutionId(pub u64);
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Event {
pub seq: u64,
pub topic: Topic,
pub data: Value,
pub execution: Option<ExecutionId>,
#[serde(skip)]
size: usize,
}
impl Event {
pub(super) fn new(
seq: u64,
topic: Topic,
data: Value,
execution: Option<ExecutionId>,
data_len: usize,
) -> Self {
let size = event_size(topic.as_str().len(), data_len);
Self {
seq,
topic,
data,
execution,
size,
}
}
pub fn size(&self) -> usize {
self.size
}
}
pub(super) fn event_size(topic_len: usize, data_len: usize) -> usize {
ENVELOPE_OVERHEAD_BYTES
.saturating_add(topic_len)
.saturating_add(data_len)
}
pub(super) fn json_len_within(value: &Value, limit: usize) -> Option<usize> {
struct Counter {
len: usize,
limit: usize,
}
impl std::io::Write for Counter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.len += buf.len();
if self.len > self.limit {
return Err(std::io::Error::other("size limit exceeded"));
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut counter = Counter { len: 0, limit };
serde_json::to_writer(&mut counter, value).ok()?;
Some(counter.len)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ipc::server::EventNotification;
use serde_json::json;
#[test]
fn event_size_covers_the_encoded_notification_line() {
let topics = ["a".to_owned(), "a".repeat(MAX_TOPIC_BYTES)];
let payloads = [
json!(null),
json!({"k": "v", "n": [1, 2, 3]}),
json!("text"),
];
for topic in &topics {
for data in &payloads {
let data_len = json_len_within(data, usize::MAX).unwrap();
let event = Event::new(
u64::MAX,
Topic::internal(topic).unwrap(),
data.clone(),
Some(ExecutionId(u64::MAX)),
data_len,
);
let mut line = serde_json::to_string(&EventNotification::new(&event)).unwrap();
line.push('\n');
assert!(
line.len() <= event.size(),
"{} bytes written but only {} accounted: {line}",
line.len(),
event.size()
);
assert_eq!(event.size(), event_size(topic.len(), data_len));
}
}
}
}