1use serde::{Deserialize, Serialize};
19use serde_json::Value;
20
21use crate::error::VtaError;
22
23pub const TSP_SERVICE_TYPE: &str = "TSPTransport";
29
30pub const DIDCOMM_SERVICE_TYPE: &str = "DIDCommMessaging";
32
33pub const REST_SERVICE_TYPE: &str = "VTARest";
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "lowercase")]
42pub enum Protocol {
43 Tsp,
44 Didcomm,
45 Rest,
46}
47
48impl Protocol {
49 pub const PREFERENCE_ORDER: [Protocol; 3] = [Protocol::Tsp, Protocol::Didcomm, Protocol::Rest];
51
52 #[must_use]
54 pub fn as_str(self) -> &'static str {
55 match self {
56 Protocol::Tsp => "tsp",
57 Protocol::Didcomm => "didcomm",
58 Protocol::Rest => "rest",
59 }
60 }
61}
62
63impl std::fmt::Display for Protocol {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.write_str(self.as_str())
66 }
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct ServiceCapabilities {
79 pub tsp: Option<String>,
80 pub didcomm: Option<String>,
81 pub rest: Option<String>,
82}
83
84impl ServiceCapabilities {
85 #[must_use]
93 pub fn from_did_document(doc: &Value) -> Self {
94 let mut caps = ServiceCapabilities::default();
95 let Some(services) = doc.get("service").and_then(Value::as_array) else {
96 return caps;
97 };
98 for svc in services {
99 let Some(uri) = svc.get("serviceEndpoint").and_then(endpoint_uri) else {
100 continue;
101 };
102 if uri.is_empty() {
103 continue;
104 }
105 if service_has_type(svc, TSP_SERVICE_TYPE) {
106 caps.tsp.get_or_insert(uri);
107 } else if service_has_type(svc, DIDCOMM_SERVICE_TYPE) {
108 caps.didcomm.get_or_insert(uri);
109 } else if service_has_type(svc, REST_SERVICE_TYPE) {
110 caps.rest.get_or_insert(uri);
111 }
112 }
113 caps
114 }
115
116 #[must_use]
119 pub fn endpoint(&self, protocol: Protocol) -> Option<&str> {
120 match protocol {
121 Protocol::Tsp => self.tsp.as_deref(),
122 Protocol::Didcomm => self.didcomm.as_deref(),
123 Protocol::Rest => self.rest.as_deref(),
124 }
125 }
126
127 #[must_use]
129 pub fn advertised(&self) -> Vec<Protocol> {
130 Protocol::PREFERENCE_ORDER
131 .into_iter()
132 .filter(|p| self.endpoint(*p).is_some())
133 .collect()
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ProtocolMatch {
140 pub protocol: Protocol,
142 pub peer_endpoint: String,
146}
147
148pub fn select_protocol(
156 ours: &ServiceCapabilities,
157 theirs: &ServiceCapabilities,
158 counterparty_did: &str,
159) -> Result<ProtocolMatch, VtaError> {
160 for protocol in Protocol::PREFERENCE_ORDER {
161 if ours.endpoint(protocol).is_some()
162 && let Some(peer) = theirs.endpoint(protocol)
163 {
164 return Ok(ProtocolMatch {
165 protocol,
166 peer_endpoint: peer.to_string(),
167 });
168 }
169 }
170 Err(VtaError::NoMatchingProtocol {
171 counterparty_did: counterparty_did.to_string(),
172 ours: ours.advertised(),
173 theirs: theirs.advertised(),
174 })
175}
176
177fn service_has_type(svc: &Value, type_: &str) -> bool {
180 match svc.get("type") {
181 Some(Value::String(s)) => s == type_,
182 Some(Value::Array(arr)) => arr.iter().any(|t| t.as_str() == Some(type_)),
183 _ => false,
184 }
185}
186
187fn endpoint_uri(endpoint: &Value) -> Option<String> {
193 match endpoint {
194 Value::String(s) => Some(s.clone()),
195 Value::Object(map) => map.get("uri")?.as_str().map(str::to_string),
196 Value::Array(arr) => arr.iter().find_map(endpoint_uri),
197 _ => None,
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use serde_json::json;
205
206 fn doc(services: Value) -> Value {
207 json!({ "id": "did:webvh:peer", "service": services })
208 }
209
210 #[test]
211 fn protocol_preference_order_is_tsp_didcomm_rest() {
212 assert_eq!(
213 Protocol::PREFERENCE_ORDER,
214 [Protocol::Tsp, Protocol::Didcomm, Protocol::Rest]
215 );
216 assert!(Protocol::Tsp < Protocol::Didcomm);
218 assert!(Protocol::Didcomm < Protocol::Rest);
219 }
220
221 #[test]
222 fn parses_each_type_and_endpoint_shape() {
223 let caps = ServiceCapabilities::from_did_document(&doc(json!([
224 { "id": "did:webvh:peer#tsp", "type": "TSPTransport",
226 "serviceEndpoint": "did:webvh:med-tsp" },
227 { "id": "did:webvh:peer#vta-didcomm", "type": "DIDCommMessaging",
229 "serviceEndpoint": [{ "accept": ["didcomm/v2"], "uri": "did:webvh:med-dc" }] },
230 { "id": "did:webvh:peer#vta-rest", "type": "VTARest",
232 "serviceEndpoint": "https://peer.example/" },
233 ])));
234 assert_eq!(caps.tsp.as_deref(), Some("did:webvh:med-tsp"));
235 assert_eq!(caps.didcomm.as_deref(), Some("did:webvh:med-dc"));
236 assert_eq!(caps.rest.as_deref(), Some("https://peer.example/"));
237 assert_eq!(
238 caps.advertised(),
239 vec![Protocol::Tsp, Protocol::Didcomm, Protocol::Rest]
240 );
241 }
242
243 #[test]
244 fn matches_by_type_not_id() {
245 let caps = ServiceCapabilities::from_did_document(&doc(json!([
249 { "id": "did:webvh:peer#tsp-transport", "type": "TSPTransport",
250 "serviceEndpoint": "did:webvh:med" },
251 { "id": "did:webvh:peer#tsp", "type": "SomethingElse",
252 "serviceEndpoint": "https://decoy.example/" },
253 ])));
254 assert_eq!(caps.tsp.as_deref(), Some("did:webvh:med"));
255 assert_eq!(caps.rest, None);
256 assert_eq!(caps.didcomm, None);
257 }
258
259 #[test]
260 fn type_may_be_an_array() {
261 let caps = ServiceCapabilities::from_did_document(&doc(json!([
262 { "id": "x", "type": ["DIDCommMessaging", "OtherThing"],
263 "serviceEndpoint": { "uri": "did:webvh:med" } },
264 ])));
265 assert_eq!(caps.didcomm.as_deref(), Some("did:webvh:med"));
266 }
267
268 #[test]
269 fn empty_or_missing_service_array_is_no_capabilities() {
270 assert_eq!(
271 ServiceCapabilities::from_did_document(&json!({ "id": "did:x" })),
272 ServiceCapabilities::default()
273 );
274 assert!(
275 ServiceCapabilities::from_did_document(&doc(json!([])))
276 .advertised()
277 .is_empty()
278 );
279 }
280
281 fn caps(tsp: Option<&str>, didcomm: Option<&str>, rest: Option<&str>) -> ServiceCapabilities {
282 ServiceCapabilities {
283 tsp: tsp.map(str::to_string),
284 didcomm: didcomm.map(str::to_string),
285 rest: rest.map(str::to_string),
286 }
287 }
288
289 #[test]
290 fn select_prefers_tsp_when_both_advertise_it() {
291 let ours = caps(
292 Some("did:m:ours"),
293 Some("did:dc:ours"),
294 Some("https://ours"),
295 );
296 let theirs = caps(
297 Some("did:m:theirs"),
298 Some("did:dc:theirs"),
299 Some("https://theirs"),
300 );
301 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
302 assert_eq!(m.protocol, Protocol::Tsp);
303 assert_eq!(m.peer_endpoint, "did:m:theirs");
305 }
306
307 #[test]
308 fn select_falls_through_to_didcomm_then_rest() {
309 let ours = caps(None, Some("did:dc:ours"), Some("https://ours"));
311 let theirs = caps(Some("did:m:theirs"), Some("did:dc:theirs"), None);
312 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
313 assert_eq!(m.protocol, Protocol::Didcomm);
314 assert_eq!(m.peer_endpoint, "did:dc:theirs");
315
316 let ours = caps(Some("did:m:ours"), None, Some("https://ours"));
318 let theirs = caps(None, Some("did:dc:theirs"), Some("https://theirs"));
319 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
320 assert_eq!(m.protocol, Protocol::Rest);
321 assert_eq!(m.peer_endpoint, "https://theirs");
322 }
323
324 #[test]
325 fn select_requires_both_sides_to_advertise() {
326 let ours = caps(Some("did:m:ours"), None, None);
328 let theirs = caps(None, None, Some("https://theirs"));
329 let err = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap_err();
330 match err {
331 VtaError::NoMatchingProtocol {
332 counterparty_did,
333 ours,
334 theirs,
335 } => {
336 assert_eq!(counterparty_did, "did:webvh:peer");
337 assert_eq!(ours, vec![Protocol::Tsp]);
338 assert_eq!(theirs, vec![Protocol::Rest]);
339 }
340 other => panic!("expected NoMatchingProtocol, got {other:?}"),
341 }
342 }
343}