use alloc::format;
use alloc::string::String;
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::types::MetadataMap;
pub const MAX_SERVICE_ID_LEN: usize = 256;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
pub struct ServiceId(String);
impl TryFrom<String> for ServiceId {
type Error = Error;
fn try_from(s: String) -> core::result::Result<Self, Self::Error> {
ServiceId::new(s)
}
}
impl<'de> Deserialize<'de> for ServiceId {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
ServiceId::new(s).map_err(serde::de::Error::custom)
}
}
impl ServiceId {
pub fn new(id: impl Into<String>) -> Result<Self, Error> {
let id = id.into();
if id.is_empty() {
return Err(Error::InvalidServiceId("service ID cannot be empty".into()));
}
if id.trim().is_empty() {
return Err(Error::InvalidServiceId(
"service ID cannot be whitespace-only".into(),
));
}
if id.len() > MAX_SERVICE_ID_LEN {
return Err(Error::InvalidServiceId(format!(
"service ID exceeds maximum length of {MAX_SERVICE_ID_LEN}"
)));
}
if id.starts_with("__") {
return Err(Error::InvalidServiceId(
"service ID cannot start with '__' (reserved for protocol internals)".into(),
));
}
Ok(Self(id))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl core::fmt::Display for ServiceId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceDescriptor {
pub service_id: ServiceId,
pub version: String,
pub capabilities: MetadataMap,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_service_id_valid() {
let id = ServiceId::new("my.service").unwrap();
assert_eq!(id.as_str(), "my.service");
assert_eq!(id.to_string(), "my.service");
}
#[test]
fn test_service_id_empty_rejected() {
let err = ServiceId::new("").unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
}
#[test]
fn test_service_id_whitespace_only_rejected() {
let err = ServiceId::new(" ").unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
let err = ServiceId::new("\t\n").unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
}
#[test]
fn test_service_id_too_long_rejected() {
let long_id = "a".repeat(MAX_SERVICE_ID_LEN + 1);
let err = ServiceId::new(long_id).unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
}
#[test]
fn test_service_id_max_length_accepted() {
let max_id = "a".repeat(MAX_SERVICE_ID_LEN);
let id = ServiceId::new(max_id).unwrap();
assert_eq!(id.as_str().len(), MAX_SERVICE_ID_LEN);
}
#[test]
fn test_service_id_reserved_prefix_rejected() {
let err = ServiceId::new("__SVC_DISC_Q__").unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
let err = ServiceId::new("__internal").unwrap_err();
assert!(matches!(err, Error::InvalidServiceId(_)));
}
#[test]
fn test_service_id_single_underscore_accepted() {
let id = ServiceId::new("_my_service").unwrap();
assert_eq!(id.as_str(), "_my_service");
}
#[test]
fn test_service_id_serde_roundtrip() {
let id = ServiceId::new("weather.v1").unwrap();
let json = serde_json::to_string(&id).unwrap();
let parsed: ServiceId = serde_json::from_str(&json).unwrap();
assert_eq!(id, parsed);
}
#[test]
fn test_service_id_empty_deserialization_rejected() {
let result: core::result::Result<ServiceId, _> = serde_json::from_str("\"\"");
assert!(result.is_err());
}
#[test]
fn test_service_descriptor_serde_roundtrip() {
let mut caps = MetadataMap::new();
caps.insert("format".to_string(), "json".to_string());
let desc = ServiceDescriptor {
service_id: ServiceId::new("echo").unwrap(),
version: "1.0".to_string(),
capabilities: caps,
};
let json = serde_json::to_string(&desc).unwrap();
let parsed: ServiceDescriptor = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.service_id.as_str(), "echo");
assert_eq!(parsed.version, "1.0");
assert_eq!(parsed.capabilities.get("format").unwrap(), "json");
}
}