anp 0.8.8

Rust SDK for Agent Network Protocol (ANP)
Documentation
use super::errors::DirectE2eeError;
use crate::authentication::ANP_MESSAGE_SERVICE_TYPE;
use serde_json::Value;
use std::collections::BTreeSet;

pub fn message_service_did_from_document(did_document: &Value) -> Result<String, DirectE2eeError> {
    let services = did_document
        .get("service")
        .and_then(Value::as_array)
        .ok_or(DirectE2eeError::MissingField("service"))?;

    let candidates: Vec<String> = services
        .iter()
        .filter_map(Value::as_object)
        .filter(|service| string_value(service.get("type")) == ANP_MESSAGE_SERVICE_TYPE)
        .filter_map(|service| {
            let service_did = string_value(service.get("serviceDid"));
            (!service_did.is_empty()).then_some(service_did)
        })
        .collect();

    match candidates.len() {
        0 => Err(DirectE2eeError::MissingField("serviceDid")),
        1 => Ok(candidates[0].clone()),
        _ => {
            let unique: BTreeSet<&str> = candidates.iter().map(String::as_str).collect();
            if unique.len() == 1 {
                Ok(candidates[0].clone())
            } else {
                Err(DirectE2eeError::invalid_field(
                    "ANPMessageService.serviceDid",
                ))
            }
        }
    }
}

fn string_value(value: Option<&Value>) -> String {
    value.and_then(Value::as_str).unwrap_or_default().to_owned()
}

#[cfg(test)]
mod tests {
    use super::message_service_did_from_document;
    use serde_json::json;

    #[test]
    fn message_service_did_from_document_matches_go_service_did_selection() {
        let service_did = message_service_did_from_document(&json!({
            "id": "did:wba:b.example:agents:bob:e1_bob",
            "service": [
                {
                    "id": "#profile",
                    "type": "ProfileService",
                    "serviceDid": "did:wba:b.example:profile"
                },
                {
                    "id": "#direct",
                    "type": "ANPMessageService",
                    "serviceEndpoint": "https://b.example/anp-im/rpc",
                    "serviceDid": "did:wba:b.example",
                    "profiles": ["anp.direct.base.v1"],
                    "securityProfiles": ["transport-protected"]
                }
            ]
        }))
        .expect("message service DID");

        assert_eq!(service_did, "did:wba:b.example");
    }

    #[test]
    fn message_service_did_from_document_accepts_duplicate_same_service_did() {
        let service_did = message_service_did_from_document(&json!({
            "service": [
                {
                    "type": "ANPMessageService",
                    "serviceDid": "did:wba:b.example",
                    "priority": 1
                },
                {
                    "type": "ANPMessageService",
                    "serviceDid": "did:wba:b.example",
                    "priority": 2
                }
            ]
        }))
        .expect("duplicate same service DID");

        assert_eq!(service_did, "did:wba:b.example");
    }

    #[test]
    fn message_service_did_from_document_rejects_missing_or_ambiguous_service_did() {
        let missing_service = message_service_did_from_document(&json!({}))
            .expect_err("missing service array should fail");
        assert!(missing_service
            .to_string()
            .contains("missing field: service"));

        let missing_service_did = message_service_did_from_document(&json!({
            "service": [
                {"type": "ANPMessageService", "serviceEndpoint": "https://b.example/rpc"}
            ]
        }))
        .expect_err("missing serviceDid should fail");
        assert!(missing_service_did
            .to_string()
            .contains("missing field: serviceDid"));

        let ambiguous = message_service_did_from_document(&json!({
            "service": [
                {"type": "ANPMessageService", "serviceDid": "did:wba:b.example"},
                {"type": "ANPMessageService", "serviceDid": "did:wba:alt.example"}
            ]
        }))
        .expect_err("distinct serviceDids should be ambiguous");
        assert!(ambiguous
            .to_string()
            .contains("invalid field: ANPMessageService.serviceDid"));

        let non_string = message_service_did_from_document(&json!({
            "service": [
                {"type": "ANPMessageService", "serviceDid": 123}
            ]
        }))
        .expect_err("non-string serviceDid should fail");
        assert!(non_string.to_string().contains("missing field: serviceDid"));
    }
}