oas-document 1.1.1

OAS Identity Document types and validation for the Open Agent Specification
Documentation
// OAS Spec §5.6 — Service Endpoint Types
//! Service endpoint types for OAS Identity Documents.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

/// A service endpoint in an OAS Identity Document.
///
/// See OAS Specification §5.6 for standard service types.
///
/// # Examples
///
/// ```
/// use oas_document::service::ServiceEndpoint;
///
/// let svc = ServiceEndpoint {
///     id: "did:oas:ns:agent:bot#api".to_string(),
///     service_type: "OASEntity".to_string(),
///     service_endpoint: "https://example.com/bot".to_string(),
///     label: Some("Primary API".to_string()),
///     description: Some("Main agent interaction endpoint".to_string()),
///     properties: None,
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceEndpoint {
    /// The service ID (e.g., `did:oas:ns:agent:bot#service-1`).
    pub id: String,

    /// The service type.
    #[serde(rename = "type")]
    pub service_type: String,

    /// The service endpoint URI.
    pub service_endpoint: String,

    /// Human-readable label for this service.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub label: Option<String>,

    /// Human-readable description of what this service does.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,

    /// Arbitrary key-value properties for this service.
    /// Useful for protocol-specific metadata (e.g., API version, auth scheme).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub properties: Option<BTreeMap<String, String>>,
}

/// Standard service types defined by OAS Specification §5.6.
pub mod service_types {
    /// Primary entity interaction endpoint.
    pub const OAS_ENTITY: &str = "OASEntity";
    /// Communication / messaging endpoint.
    pub const OAS_COMMUNICATION: &str = "OASCommunication";
    /// Lineage chain query service.
    pub const OAS_LINEAGE_QUERY: &str = "OASLineageQuery";
    /// Third-party attestation service.
    pub const OAS_ATTESTATION: &str = "OASAttestationService";
    /// Revocation status check endpoint.
    pub const OAS_REVOCATION_STATUS: &str = "OASRevocationStatus";
    /// Capability invocation endpoint.
    pub const OAS_CAPABILITY_INVOCATION: &str = "OASCapabilityInvocation";
    /// Governance operations endpoint.
    pub const OAS_GOVERNANCE: &str = "OASGovernance";
    /// Authenticated endpoint for resolving visibility-restricted fields.
    /// Agents query this service with DID-Auth to retrieve fields that are
    /// redacted in the public DHT document.
    pub const OAS_PRIVATE_ENDPOINT: &str = "OASPrivateEndpointService";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn serde_round_trip() {
        let svc = ServiceEndpoint {
            id: "did:oas:ns:agent:bot#service-1".to_string(),
            service_type: "OASEntity".to_string(),
            service_endpoint: "https://example.com/bot".to_string(),
            label: None,
            description: None,
            properties: None,
        };
        let json = serde_json::to_string(&svc).unwrap_or_default();
        let parsed: ServiceEndpoint = serde_json::from_str(&json).unwrap_or_else(|_| svc.clone());
        assert_eq!(svc, parsed);
    }

    #[test]
    fn backward_compat_3_fields_only() {
        // Existing documents with only 3 fields must deserialize cleanly.
        let json = r#"{"id":"did:oas:ns:agent:bot#s1","type":"OASEntity","serviceEndpoint":"https://example.com"}"#;
        let svc: Result<ServiceEndpoint, _> = serde_json::from_str(json);
        assert!(svc.is_ok());
        let svc = svc.unwrap_or_else(|_| ServiceEndpoint {
            id: String::new(),
            service_type: String::new(),
            service_endpoint: String::new(),
            label: None,
            description: None,
            properties: None,
        });
        assert!(svc.label.is_none());
        assert!(svc.properties.is_none());
    }

    #[test]
    fn with_properties() {
        let mut props = BTreeMap::new();
        props.insert("apiVersion".to_string(), "v2".to_string());
        props.insert("authScheme".to_string(), "did-auth".to_string());

        let svc = ServiceEndpoint {
            id: "did:oas:ns:agent:bot#api".to_string(),
            service_type: service_types::OAS_ENTITY.to_string(),
            service_endpoint: "https://example.com/bot/v2".to_string(),
            label: Some("API v2".to_string()),
            description: Some("Production API endpoint".to_string()),
            properties: Some(props),
        };

        let json = serde_json::to_string_pretty(&svc).unwrap_or_default();
        let parsed: ServiceEndpoint = serde_json::from_str(&json).unwrap_or_else(|_| svc.clone());
        assert_eq!(svc, parsed);
        assert!(json.contains("apiVersion"));
    }

    #[test]
    fn private_endpoint_service_type() {
        let svc = ServiceEndpoint {
            id: "did:oas:ns:agent:bot#private".to_string(),
            service_type: service_types::OAS_PRIVATE_ENDPOINT.to_string(),
            service_endpoint: "https://example.com/bot/private".to_string(),
            label: Some("Private field resolver".to_string()),
            description: None,
            properties: None,
        };
        assert_eq!(svc.service_type, "OASPrivateEndpointService");
    }
}