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";
40
41pub const TRQP_REST_SERVICE_TYPE: &str = "TRQPRest";
50
51pub const REST_SERVICE_TYPES: [&str; 2] = [REST_SERVICE_TYPE, TRQP_REST_SERVICE_TYPE];
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum Protocol {
65 Tsp,
66 Didcomm,
67 Rest,
68}
69
70impl Protocol {
71 pub const PREFERENCE_ORDER: [Protocol; 3] = [Protocol::Tsp, Protocol::Didcomm, Protocol::Rest];
73
74 #[must_use]
76 pub fn as_str(self) -> &'static str {
77 match self {
78 Protocol::Tsp => "tsp",
79 Protocol::Didcomm => "didcomm",
80 Protocol::Rest => "rest",
81 }
82 }
83}
84
85impl std::fmt::Display for Protocol {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(self.as_str())
88 }
89}
90
91#[derive(Debug, Clone, Default, PartialEq, Eq)]
100pub struct ServiceCapabilities {
101 pub tsp: Option<String>,
102 pub didcomm: Option<String>,
103 pub rest: Option<String>,
104}
105
106impl ServiceCapabilities {
107 #[must_use]
115 pub fn from_did_document(doc: &Value) -> Self {
116 let mut caps = ServiceCapabilities::default();
117 let Some(services) = doc.get("service").and_then(Value::as_array) else {
118 return caps;
119 };
120 for svc in services {
121 let Some(uri) = svc.get("serviceEndpoint").and_then(endpoint_uri) else {
122 continue;
123 };
124 if uri.is_empty() {
125 continue;
126 }
127 if service_has_type(svc, TSP_SERVICE_TYPE) {
128 caps.tsp.get_or_insert(uri);
129 } else if service_has_type(svc, DIDCOMM_SERVICE_TYPE) {
130 caps.didcomm.get_or_insert(uri);
131 } else if REST_SERVICE_TYPES.iter().any(|t| service_has_type(svc, t)) {
132 caps.rest.get_or_insert(uri);
133 }
134 }
135 caps
136 }
137
138 #[must_use]
141 pub fn endpoint(&self, protocol: Protocol) -> Option<&str> {
142 match protocol {
143 Protocol::Tsp => self.tsp.as_deref(),
144 Protocol::Didcomm => self.didcomm.as_deref(),
145 Protocol::Rest => self.rest.as_deref(),
146 }
147 }
148
149 #[must_use]
151 pub fn advertised(&self) -> Vec<Protocol> {
152 Protocol::PREFERENCE_ORDER
153 .into_iter()
154 .filter(|p| self.endpoint(*p).is_some())
155 .collect()
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct ProtocolMatch {
162 pub protocol: Protocol,
164 pub peer_endpoint: String,
168}
169
170pub fn select_protocol(
178 ours: &ServiceCapabilities,
179 theirs: &ServiceCapabilities,
180 counterparty_did: &str,
181) -> Result<ProtocolMatch, VtaError> {
182 for protocol in Protocol::PREFERENCE_ORDER {
183 if ours.endpoint(protocol).is_some()
184 && let Some(peer) = theirs.endpoint(protocol)
185 {
186 return Ok(ProtocolMatch {
187 protocol,
188 peer_endpoint: peer.to_string(),
189 });
190 }
191 }
192 Err(VtaError::NoMatchingProtocol {
193 counterparty_did: counterparty_did.to_string(),
194 ours: ours.advertised(),
195 theirs: theirs.advertised(),
196 })
197}
198
199fn service_has_type(svc: &Value, type_: &str) -> bool {
202 match svc.get("type") {
203 Some(Value::String(s)) => s == type_,
204 Some(Value::Array(arr)) => arr.iter().any(|t| t.as_str() == Some(type_)),
205 _ => false,
206 }
207}
208
209fn endpoint_uri(endpoint: &Value) -> Option<String> {
215 match endpoint {
216 Value::String(s) => Some(s.clone()),
217 Value::Object(map) => map.get("uri")?.as_str().map(str::to_string),
218 Value::Array(arr) => arr.iter().find_map(endpoint_uri),
219 _ => None,
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use serde_json::json;
227
228 fn doc(services: Value) -> Value {
229 json!({ "id": "did:webvh:peer", "service": services })
230 }
231
232 #[test]
236 fn both_rest_service_types_are_recognised() {
237 for ty in ["VTARest", "TRQPRest"] {
238 let caps = ServiceCapabilities::from_did_document(&doc(json!([
239 { "id": "did:webvh:peer#rest", "type": ty,
240 "serviceEndpoint": "https://peer.example" }
241 ])));
242 assert_eq!(
243 caps.rest.as_deref(),
244 Some("https://peer.example"),
245 "{ty} must be recognised as a REST endpoint"
246 );
247 assert_eq!(caps.advertised(), vec![Protocol::Rest]);
248 }
249 }
250
251 #[test]
254 fn trqp_rest_only_peer_is_selectable() {
255 let theirs = ServiceCapabilities::from_did_document(&doc(json!([
256 { "id": "did:webvh:registry#rest", "type": "TRQPRest",
257 "serviceEndpoint": "https://registry.example" }
258 ])));
259 let ours = ServiceCapabilities {
260 rest: Some("https://us.example".into()),
261 ..Default::default()
262 };
263 let chosen = select_protocol(&ours, &theirs, "did:webvh:registry").unwrap();
264 assert_eq!(chosen.protocol, Protocol::Rest);
265 assert_eq!(chosen.peer_endpoint, "https://registry.example");
266 }
267
268 #[test]
269 fn protocol_preference_order_is_tsp_didcomm_rest() {
270 assert_eq!(
271 Protocol::PREFERENCE_ORDER,
272 [Protocol::Tsp, Protocol::Didcomm, Protocol::Rest]
273 );
274 assert!(Protocol::Tsp < Protocol::Didcomm);
276 assert!(Protocol::Didcomm < Protocol::Rest);
277 }
278
279 #[test]
280 fn parses_each_type_and_endpoint_shape() {
281 let caps = ServiceCapabilities::from_did_document(&doc(json!([
282 { "id": "did:webvh:peer#tsp", "type": "TSPTransport",
284 "serviceEndpoint": "did:webvh:med-tsp" },
285 { "id": "did:webvh:peer#vta-didcomm", "type": "DIDCommMessaging",
287 "serviceEndpoint": [{ "accept": ["didcomm/v2"], "uri": "did:webvh:med-dc" }] },
288 { "id": "did:webvh:peer#vta-rest", "type": "VTARest",
290 "serviceEndpoint": "https://peer.example/" },
291 ])));
292 assert_eq!(caps.tsp.as_deref(), Some("did:webvh:med-tsp"));
293 assert_eq!(caps.didcomm.as_deref(), Some("did:webvh:med-dc"));
294 assert_eq!(caps.rest.as_deref(), Some("https://peer.example/"));
295 assert_eq!(
296 caps.advertised(),
297 vec![Protocol::Tsp, Protocol::Didcomm, Protocol::Rest]
298 );
299 }
300
301 #[test]
302 fn matches_by_type_not_id() {
303 let caps = ServiceCapabilities::from_did_document(&doc(json!([
307 { "id": "did:webvh:peer#tsp-transport", "type": "TSPTransport",
308 "serviceEndpoint": "did:webvh:med" },
309 { "id": "did:webvh:peer#tsp", "type": "SomethingElse",
310 "serviceEndpoint": "https://decoy.example/" },
311 ])));
312 assert_eq!(caps.tsp.as_deref(), Some("did:webvh:med"));
313 assert_eq!(caps.rest, None);
314 assert_eq!(caps.didcomm, None);
315 }
316
317 #[test]
318 fn type_may_be_an_array() {
319 let caps = ServiceCapabilities::from_did_document(&doc(json!([
320 { "id": "x", "type": ["DIDCommMessaging", "OtherThing"],
321 "serviceEndpoint": { "uri": "did:webvh:med" } },
322 ])));
323 assert_eq!(caps.didcomm.as_deref(), Some("did:webvh:med"));
324 }
325
326 #[test]
327 fn empty_or_missing_service_array_is_no_capabilities() {
328 assert_eq!(
329 ServiceCapabilities::from_did_document(&json!({ "id": "did:x" })),
330 ServiceCapabilities::default()
331 );
332 assert!(
333 ServiceCapabilities::from_did_document(&doc(json!([])))
334 .advertised()
335 .is_empty()
336 );
337 }
338
339 fn caps(tsp: Option<&str>, didcomm: Option<&str>, rest: Option<&str>) -> ServiceCapabilities {
340 ServiceCapabilities {
341 tsp: tsp.map(str::to_string),
342 didcomm: didcomm.map(str::to_string),
343 rest: rest.map(str::to_string),
344 }
345 }
346
347 #[test]
348 fn select_prefers_tsp_when_both_advertise_it() {
349 let ours = caps(
350 Some("did:m:ours"),
351 Some("did:dc:ours"),
352 Some("https://ours"),
353 );
354 let theirs = caps(
355 Some("did:m:theirs"),
356 Some("did:dc:theirs"),
357 Some("https://theirs"),
358 );
359 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
360 assert_eq!(m.protocol, Protocol::Tsp);
361 assert_eq!(m.peer_endpoint, "did:m:theirs");
363 }
364
365 #[test]
366 fn select_falls_through_to_didcomm_then_rest() {
367 let ours = caps(None, Some("did:dc:ours"), Some("https://ours"));
369 let theirs = caps(Some("did:m:theirs"), Some("did:dc:theirs"), None);
370 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
371 assert_eq!(m.protocol, Protocol::Didcomm);
372 assert_eq!(m.peer_endpoint, "did:dc:theirs");
373
374 let ours = caps(Some("did:m:ours"), None, Some("https://ours"));
376 let theirs = caps(None, Some("did:dc:theirs"), Some("https://theirs"));
377 let m = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap();
378 assert_eq!(m.protocol, Protocol::Rest);
379 assert_eq!(m.peer_endpoint, "https://theirs");
380 }
381
382 #[test]
383 fn select_requires_both_sides_to_advertise() {
384 let ours = caps(Some("did:m:ours"), None, None);
386 let theirs = caps(None, None, Some("https://theirs"));
387 let err = select_protocol(&ours, &theirs, "did:webvh:peer").unwrap_err();
388 match err {
389 VtaError::NoMatchingProtocol {
390 counterparty_did,
391 ours,
392 theirs,
393 } => {
394 assert_eq!(counterparty_did, "did:webvh:peer");
395 assert_eq!(ours, vec![Protocol::Tsp]);
396 assert_eq!(theirs, vec![Protocol::Rest]);
397 }
398 other => panic!("expected NoMatchingProtocol, got {other:?}"),
399 }
400 }
401}