Skip to main content

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}