oas_document/interop.rs
1// OAS Spec §5.13 — Interoperability
2//! Interoperability section for OAS Identity Documents.
3//!
4//! Describes "what I speak" — supported protocols, API schema URLs,
5//! and authentication methods this entity accepts.
6
7use serde::{Deserialize, Serialize};
8
9/// A supported protocol with optional version and endpoint.
10///
11/// # Examples
12///
13/// ```
14/// use oas_document::interop::ProtocolSupport;
15///
16/// let proto = ProtocolSupport {
17/// protocol: "mcp".to_string(),
18/// version: Some("2025-01".to_string()),
19/// endpoint: Some("https://agent.acme.com/mcp".to_string()),
20/// };
21/// ```
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct ProtocolSupport {
25 /// Protocol identifier: `"mcp"`, `"a2a"`, `"rest"`, `"graphql"`,
26 /// `"grpc"`, `"websocket"`, `"oas-map"`.
27 pub protocol: String,
28
29 /// Protocol version if applicable.
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub version: Option<String>,
32
33 /// Endpoint URL for this protocol.
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub endpoint: Option<String>,
36}
37
38/// An API schema reference.
39///
40/// # Examples
41///
42/// ```
43/// use oas_document::interop::ApiSchema;
44///
45/// let schema = ApiSchema {
46/// schema_type: "openapi".to_string(),
47/// url: "https://agent.acme.com/openapi.json".to_string(),
48/// version: Some("3.1.0".to_string()),
49/// };
50/// ```
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct ApiSchema {
54 /// Schema format: `"openapi"`, `"asyncapi"`, `"json-schema"`,
55 /// `"protobuf"`, `"graphql-sdl"`.
56 pub schema_type: String,
57
58 /// URL to the schema document.
59 pub url: String,
60
61 /// Schema version.
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub version: Option<String>,
64}
65
66/// Interoperability section of an OAS Identity Document.
67///
68/// # Examples
69///
70/// ```
71/// use oas_document::interop::{InteroperabilitySection, ProtocolSupport, ApiSchema};
72///
73/// let interop = InteroperabilitySection {
74/// protocols: vec![
75/// ProtocolSupport {
76/// protocol: "mcp".to_string(),
77/// version: Some("2025-01".to_string()),
78/// endpoint: Some("https://agent.acme.com/mcp".to_string()),
79/// },
80/// ProtocolSupport {
81/// protocol: "rest".to_string(),
82/// version: None,
83/// endpoint: Some("https://api.acme.com/v1".to_string()),
84/// },
85/// ],
86/// api_schemas: vec![ApiSchema {
87/// schema_type: "openapi".to_string(),
88/// url: "https://api.acme.com/v1/openapi.json".to_string(),
89/// version: Some("3.1.0".to_string()),
90/// }],
91/// auth_methods: vec!["did-auth".to_string(), "api-key".to_string()],
92/// };
93/// assert_eq!(interop.protocols.len(), 2);
94/// ```
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct InteroperabilitySection {
98 /// Protocols this entity supports.
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
100 pub protocols: Vec<ProtocolSupport>,
101
102 /// API schema references.
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub api_schemas: Vec<ApiSchema>,
105
106 /// Authentication methods accepted: `"did-auth"`, `"api-key"`,
107 /// `"oauth2"`, `"mtls"`, `"bearer-token"`.
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub auth_methods: Vec<String>,
110}
111
112/// Standard protocol identifiers.
113pub mod protocols {
114 /// Model Context Protocol.
115 pub const MCP: &str = "mcp";
116 /// Google Agent-to-Agent protocol.
117 pub const A2A: &str = "a2a";
118 /// REST / HTTP API.
119 pub const REST: &str = "rest";
120 /// GraphQL API.
121 pub const GRAPHQL: &str = "graphql";
122 /// gRPC.
123 pub const GRPC: &str = "grpc";
124 /// WebSocket.
125 pub const WEBSOCKET: &str = "websocket";
126 /// OAS MAP protocol suite.
127 pub const OAS_MAP: &str = "oas-map";
128}
129
130/// Standard authentication methods.
131pub mod auth_methods {
132 pub const DID_AUTH: &str = "did-auth";
133 pub const API_KEY: &str = "api-key";
134 pub const OAUTH2: &str = "oauth2";
135 pub const MTLS: &str = "mtls";
136 pub const BEARER_TOKEN: &str = "bearer-token";
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn serde_round_trip() {
145 let interop = InteroperabilitySection {
146 protocols: vec![ProtocolSupport {
147 protocol: protocols::MCP.to_string(),
148 version: Some("2025-01".to_string()),
149 endpoint: None,
150 }],
151 api_schemas: vec![ApiSchema {
152 schema_type: "openapi".to_string(),
153 url: "https://example.com/api.json".to_string(),
154 version: None,
155 }],
156 auth_methods: vec![auth_methods::DID_AUTH.to_string()],
157 };
158
159 let json = serde_json::to_string(&interop).unwrap_or_default();
160 let parsed: InteroperabilitySection =
161 serde_json::from_str(&json).unwrap_or_else(|_| interop.clone());
162 assert_eq!(interop, parsed);
163 }
164
165 #[test]
166 fn empty_section_deserializes() {
167 let json = r#"{}"#;
168 let interop: Result<InteroperabilitySection, _> = serde_json::from_str(json);
169 assert!(interop.is_ok());
170 }
171}