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