1use std::collections::BTreeMap;
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct ServiceEndpoint {
29 pub id: String,
31
32 #[serde(rename = "type")]
34 pub service_type: String,
35
36 pub service_endpoint: String,
38
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub label: Option<String>,
42
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub description: Option<String>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub properties: Option<BTreeMap<String, String>>,
51}
52
53pub mod service_types {
55 pub const OAS_ENTITY: &str = "OASEntity";
57 pub const OAS_COMMUNICATION: &str = "OASCommunication";
59 pub const OAS_LINEAGE_QUERY: &str = "OASLineageQuery";
61 pub const OAS_ATTESTATION: &str = "OASAttestationService";
63 pub const OAS_REVOCATION_STATUS: &str = "OASRevocationStatus";
65 pub const OAS_CAPABILITY_INVOCATION: &str = "OASCapabilityInvocation";
67 pub const OAS_GOVERNANCE: &str = "OASGovernance";
69 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 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}