use super::transport::{IpcError, IpcResult};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
Publisher,
Subscriber,
}
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Role::Publisher => "publisher",
Role::Subscriber => "subscriber",
}
}
pub fn counterpart(&self) -> Role {
match self {
Role::Publisher => Role::Subscriber,
Role::Subscriber => Role::Publisher,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SchemaId(pub String);
impl SchemaId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SchemaId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for SchemaId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EndpointUri {
pub namespace: String,
pub name: String,
pub schema_id: SchemaId,
pub msg_version: u16,
}
impl EndpointUri {
pub fn new(
namespace: impl Into<String>,
name: impl Into<String>,
schema_id: SchemaId,
msg_version: u16,
) -> Self {
Self {
namespace: namespace.into(),
name: name.into(),
schema_id,
msg_version,
}
}
pub fn parse(s: &str) -> IpcResult<Self> {
let rest = s
.strip_prefix("roplat-ipc://")
.ok_or_else(|| IpcError::InvalidUri(format!("missing scheme: {s}")))?;
let (path, query) = match rest.split_once('?') {
Some((p, q)) => (p, Some(q)),
None => (rest, None),
};
let (namespace, name) = path
.split_once('/')
.ok_or_else(|| IpcError::InvalidUri(format!("missing namespace/name: {s}")))?;
if namespace.is_empty() || name.is_empty() {
return Err(IpcError::InvalidUri(format!(
"empty namespace or name: {s}"
)));
}
let (mut schema_id, mut msg_version) = (None, None);
if let Some(q) = query {
for pair in q.split('&') {
let (k, v) = pair
.split_once('=')
.ok_or_else(|| IpcError::InvalidUri(format!("bad query pair: {pair}")))?;
match k {
"msg" => schema_id = Some(SchemaId::from(v)),
"v" => {
msg_version = Some(
v.parse::<u16>()
.map_err(|e| IpcError::InvalidUri(format!("bad version: {e}")))?,
)
}
_ => {}
}
}
}
Ok(Self {
namespace: namespace.to_string(),
name: name.to_string(),
schema_id: schema_id
.ok_or_else(|| IpcError::InvalidUri(format!("missing msg=: {s}")))?,
msg_version: msg_version
.ok_or_else(|| IpcError::InvalidUri(format!("missing v=: {s}")))?,
})
}
pub fn rendezvous_relpath(&self) -> String {
format!("{}/{}.rdv", self.namespace, self.name)
}
}
impl fmt::Display for EndpointUri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"roplat-ipc://{}/{}?msg={}&v={}",
self.namespace, self.name, self.schema_id, self.msg_version
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_roundtrip() {
let s = "roplat-ipc://default/sensor?msg=a4f1c2&v=2";
let uri = EndpointUri::parse(s).unwrap();
assert_eq!(uri.namespace, "default");
assert_eq!(uri.name, "sensor");
assert_eq!(uri.schema_id.as_str(), "a4f1c2");
assert_eq!(uri.msg_version, 2);
assert_eq!(uri.to_string(), s);
}
#[test]
fn parse_missing_scheme() {
assert!(EndpointUri::parse("default/sensor?msg=x&v=1").is_err());
}
#[test]
fn parse_missing_msg() {
assert!(EndpointUri::parse("roplat-ipc://ns/n?v=1").is_err());
}
}