Skip to main content

oas_document/
service.rs

1// OAS Spec §5.6 — Service Endpoint Types
2//! Service endpoint types for OAS Identity Documents.
3
4use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8/// A service endpoint in an OAS Identity Document.
9///
10/// See OAS Specification §5.6 for standard service types.
11///
12/// # Examples
13///
14/// ```
15/// use oas_document::service::ServiceEndpoint;
16///
17/// let svc = ServiceEndpoint {
18///     id: "did:oas:ns:agent:bot#api".to_string(),
19///     service_type: "OASEntity".to_string(),
20///     service_endpoint: "https://example.com/bot".to_string(),
21///     label: Some("Primary API".to_string()),
22///     description: Some("Main agent interaction endpoint".to_string()),
23///     properties: None,
24/// };
25/// ```
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct ServiceEndpoint {
29    /// The service ID (e.g., `did:oas:ns:agent:bot#service-1`).
30    pub id: String,
31
32    /// The service type.
33    #[serde(rename = "type")]
34    pub service_type: String,
35
36    /// The service endpoint URI.
37    pub service_endpoint: String,
38
39    /// Human-readable label for this service.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub label: Option<String>,
42
43    /// Human-readable description of what this service does.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub description: Option<String>,
46
47    /// Arbitrary key-value properties for this service.
48    /// Useful for protocol-specific metadata (e.g., API version, auth scheme).
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub properties: Option<BTreeMap<String, String>>,
51}
52
53/// Standard service types defined by OAS Specification §5.6.
54pub mod service_types {
55    /// Primary entity interaction endpoint.
56    pub const OAS_ENTITY: &str = "OASEntity";
57    /// Communication / messaging endpoint.
58    pub const OAS_COMMUNICATION: &str = "OASCommunication";
59    /// Lineage chain query service.
60    pub const OAS_LINEAGE_QUERY: &str = "OASLineageQuery";
61    /// Third-party attestation service.
62    pub const OAS_ATTESTATION: &str = "OASAttestationService";
63    /// Revocation status check endpoint.
64    pub const OAS_REVOCATION_STATUS: &str = "OASRevocationStatus";
65    /// Capability invocation endpoint.
66    pub const OAS_CAPABILITY_INVOCATION: &str = "OASCapabilityInvocation";
67    /// Governance operations endpoint.
68    pub const OAS_GOVERNANCE: &str = "OASGovernance";
69    /// Authenticated endpoint for resolving visibility-restricted fields.
70    /// Agents query this service with DID-Auth to retrieve fields that are
71    /// redacted in the public DHT document.
72    pub const OAS_PRIVATE_ENDPOINT: &str = "OASPrivateEndpointService";
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn serde_round_trip() {
81        let svc = ServiceEndpoint {
82            id: "did:oas:ns:agent:bot#service-1".to_string(),
83            service_type: "OASEntity".to_string(),
84            service_endpoint: "https://example.com/bot".to_string(),
85            label: None,
86            description: None,
87            properties: None,
88        };
89        let json = serde_json::to_string(&svc).unwrap_or_default();
90        let parsed: ServiceEndpoint = serde_json::from_str(&json).unwrap_or_else(|_| svc.clone());
91        assert_eq!(svc, parsed);
92    }
93
94    #[test]
95    fn backward_compat_3_fields_only() {
96        // Existing documents with only 3 fields must deserialize cleanly.
97        let json = r#"{"id":"did:oas:ns:agent:bot#s1","type":"OASEntity","serviceEndpoint":"https://example.com"}"#;
98        let svc: Result<ServiceEndpoint, _> = serde_json::from_str(json);
99        assert!(svc.is_ok());
100        let svc = svc.unwrap_or_else(|_| ServiceEndpoint {
101            id: String::new(),
102            service_type: String::new(),
103            service_endpoint: String::new(),
104            label: None,
105            description: None,
106            properties: None,
107        });
108        assert!(svc.label.is_none());
109        assert!(svc.properties.is_none());
110    }
111
112    #[test]
113    fn with_properties() {
114        let mut props = BTreeMap::new();
115        props.insert("apiVersion".to_string(), "v2".to_string());
116        props.insert("authScheme".to_string(), "did-auth".to_string());
117
118        let svc = ServiceEndpoint {
119            id: "did:oas:ns:agent:bot#api".to_string(),
120            service_type: service_types::OAS_ENTITY.to_string(),
121            service_endpoint: "https://example.com/bot/v2".to_string(),
122            label: Some("API v2".to_string()),
123            description: Some("Production API endpoint".to_string()),
124            properties: Some(props),
125        };
126
127        let json = serde_json::to_string_pretty(&svc).unwrap_or_default();
128        let parsed: ServiceEndpoint = serde_json::from_str(&json).unwrap_or_else(|_| svc.clone());
129        assert_eq!(svc, parsed);
130        assert!(json.contains("apiVersion"));
131    }
132
133    #[test]
134    fn private_endpoint_service_type() {
135        let svc = ServiceEndpoint {
136            id: "did:oas:ns:agent:bot#private".to_string(),
137            service_type: service_types::OAS_PRIVATE_ENDPOINT.to_string(),
138            service_endpoint: "https://example.com/bot/private".to_string(),
139            label: Some("Private field resolver".to_string()),
140            description: None,
141            properties: None,
142        };
143        assert_eq!(svc.service_type, "OASPrivateEndpointService");
144    }
145}