offline_protocol_core/
service.rs1use alloc::format;
4use alloc::string::String;
5use serde::{Deserialize, Serialize};
6
7use crate::error::Error;
8use crate::types::MetadataMap;
9
10pub const MAX_SERVICE_ID_LEN: usize = 256;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
15pub struct ServiceId(String);
16
17impl TryFrom<String> for ServiceId {
18 type Error = Error;
19
20 fn try_from(s: String) -> core::result::Result<Self, Self::Error> {
21 ServiceId::new(s)
22 }
23}
24
25impl<'de> Deserialize<'de> for ServiceId {
26 fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
27 where
28 D: serde::Deserializer<'de>,
29 {
30 let s = String::deserialize(deserializer)?;
31 ServiceId::new(s).map_err(serde::de::Error::custom)
32 }
33}
34
35impl ServiceId {
36 pub fn new(id: impl Into<String>) -> Result<Self, Error> {
42 let id = id.into();
43 if id.is_empty() {
44 return Err(Error::InvalidServiceId("service ID cannot be empty".into()));
45 }
46 if id.trim().is_empty() {
47 return Err(Error::InvalidServiceId(
48 "service ID cannot be whitespace-only".into(),
49 ));
50 }
51 if id.len() > MAX_SERVICE_ID_LEN {
52 return Err(Error::InvalidServiceId(format!(
53 "service ID exceeds maximum length of {MAX_SERVICE_ID_LEN}"
54 )));
55 }
56 if id.starts_with("__") {
57 return Err(Error::InvalidServiceId(
58 "service ID cannot start with '__' (reserved for protocol internals)".into(),
59 ));
60 }
61 Ok(Self(id))
62 }
63
64 pub fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl core::fmt::Display for ServiceId {
71 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72 write!(f, "{}", self.0)
73 }
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ServiceDescriptor {
79 pub service_id: ServiceId,
81 pub version: String,
83 pub capabilities: MetadataMap,
85}
86
87#[cfg(all(test, feature = "std"))]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn test_service_id_valid() {
93 let id = ServiceId::new("my.service").unwrap();
94 assert_eq!(id.as_str(), "my.service");
95 assert_eq!(id.to_string(), "my.service");
96 }
97
98 #[test]
99 fn test_service_id_empty_rejected() {
100 let err = ServiceId::new("").unwrap_err();
101 assert!(matches!(err, Error::InvalidServiceId(_)));
102 }
103
104 #[test]
105 fn test_service_id_whitespace_only_rejected() {
106 let err = ServiceId::new(" ").unwrap_err();
107 assert!(matches!(err, Error::InvalidServiceId(_)));
108 let err = ServiceId::new("\t\n").unwrap_err();
109 assert!(matches!(err, Error::InvalidServiceId(_)));
110 }
111
112 #[test]
113 fn test_service_id_too_long_rejected() {
114 let long_id = "a".repeat(MAX_SERVICE_ID_LEN + 1);
115 let err = ServiceId::new(long_id).unwrap_err();
116 assert!(matches!(err, Error::InvalidServiceId(_)));
117 }
118
119 #[test]
120 fn test_service_id_max_length_accepted() {
121 let max_id = "a".repeat(MAX_SERVICE_ID_LEN);
122 let id = ServiceId::new(max_id).unwrap();
123 assert_eq!(id.as_str().len(), MAX_SERVICE_ID_LEN);
124 }
125
126 #[test]
127 fn test_service_id_reserved_prefix_rejected() {
128 let err = ServiceId::new("__SVC_DISC_Q__").unwrap_err();
129 assert!(matches!(err, Error::InvalidServiceId(_)));
130 let err = ServiceId::new("__internal").unwrap_err();
131 assert!(matches!(err, Error::InvalidServiceId(_)));
132 }
133
134 #[test]
135 fn test_service_id_single_underscore_accepted() {
136 let id = ServiceId::new("_my_service").unwrap();
137 assert_eq!(id.as_str(), "_my_service");
138 }
139
140 #[test]
141 fn test_service_id_serde_roundtrip() {
142 let id = ServiceId::new("weather.v1").unwrap();
143 let json = serde_json::to_string(&id).unwrap();
144 let parsed: ServiceId = serde_json::from_str(&json).unwrap();
145 assert_eq!(id, parsed);
146 }
147
148 #[test]
149 fn test_service_id_empty_deserialization_rejected() {
150 let result: core::result::Result<ServiceId, _> = serde_json::from_str("\"\"");
151 assert!(result.is_err());
152 }
153
154 #[test]
155 fn test_service_descriptor_serde_roundtrip() {
156 let mut caps = MetadataMap::new();
157 caps.insert("format".to_string(), "json".to_string());
158 let desc = ServiceDescriptor {
159 service_id: ServiceId::new("echo").unwrap(),
160 version: "1.0".to_string(),
161 capabilities: caps,
162 };
163 let json = serde_json::to_string(&desc).unwrap();
164 let parsed: ServiceDescriptor = serde_json::from_str(&json).unwrap();
165 assert_eq!(parsed.service_id.as_str(), "echo");
166 assert_eq!(parsed.version, "1.0");
167 assert_eq!(parsed.capabilities.get("format").unwrap(), "json");
168 }
169}