use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCard {
pub name: String,
pub description: String,
pub version: String,
#[serde(rename = "supportedInterfaces")]
pub supported_interfaces: Vec<AgentInterface>,
pub capabilities: AgentCapabilities,
pub skills: Vec<AgentSkill>,
#[serde(rename = "defaultInputModes")]
pub default_input_modes: Vec<String>,
#[serde(rename = "defaultOutputModes")]
pub default_output_modes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
#[serde(rename = "securitySchemes", skip_serializing_if = "Option::is_none")]
pub security_schemes: Option<HashMap<String, SecurityScheme>>,
#[serde(
rename = "securityRequirements",
skip_serializing_if = "Option::is_none"
)]
pub security_requirements: Option<Vec<SecurityRequirement>>,
#[serde(rename = "documentationUrl", skip_serializing_if = "Option::is_none")]
pub documentation_url: Option<String>,
#[serde(rename = "iconUrl", skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signatures: Option<Vec<AgentCardSignature>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInterface {
pub url: String,
#[serde(rename = "protocolBinding")]
pub protocol_binding: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tenant: Option<String>,
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub streaming: Option<bool>,
#[serde(rename = "pushNotifications", skip_serializing_if = "Option::is_none")]
pub push_notifications: Option<bool>,
#[serde(rename = "extendedAgentCard", skip_serializing_if = "Option::is_none")]
pub extended_agent_card: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<AgentExtension>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentExtension {
pub uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSkill {
pub id: String,
pub name: String,
pub description: String,
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub examples: Option<Vec<String>>,
#[serde(rename = "inputModes", skip_serializing_if = "Option::is_none")]
pub input_modes: Option<Vec<String>>,
#[serde(rename = "outputModes", skip_serializing_if = "Option::is_none")]
pub output_modes: Option<Vec<String>>,
#[serde(
rename = "securityRequirements",
skip_serializing_if = "Option::is_none"
)]
pub security_requirements: Option<Vec<SecurityRequirement>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProvider {
pub url: String,
pub organization: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardSignature {
pub protected: String,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub header: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SecurityRequirement {
pub schemes: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SecurityScheme {
#[serde(
skip_serializing_if = "Option::is_none",
rename = "apiKeySecurityScheme"
)]
pub api_key: Option<ApiKeySecurityScheme>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "httpAuthSecurityScheme"
)]
pub http_auth: Option<HttpAuthSecurityScheme>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "oauth2SecurityScheme"
)]
pub oauth2: Option<OAuth2SecurityScheme>,
#[serde(
skip_serializing_if = "Option::is_none",
rename = "openIdConnectSecurityScheme"
)]
pub open_id_connect: Option<OpenIdConnectSecurityScheme>,
#[serde(skip_serializing_if = "Option::is_none", rename = "mtlsSecurityScheme")]
pub mtls: Option<MutualTlsSecurityScheme>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ApiKeySecurityScheme {
pub name: String,
#[serde(rename = "in")]
pub location: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HttpAuthSecurityScheme {
pub scheme: String,
#[serde(rename = "bearerFormat", skip_serializing_if = "Option::is_none")]
pub bearer_format: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OAuth2SecurityScheme {
pub flows: OAuthFlows,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(rename = "oauth2MetadataUrl", skip_serializing_if = "Option::is_none")]
pub oauth2_metadata_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OpenIdConnectSecurityScheme {
#[serde(rename = "openIdConnectUrl")]
pub open_id_connect_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MutualTlsSecurityScheme {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OAuthFlows {
#[serde(skip_serializing_if = "Option::is_none", rename = "authorizationCode")]
pub authorization_code: Option<AuthorizationCodeOAuthFlow>,
#[serde(skip_serializing_if = "Option::is_none", rename = "clientCredentials")]
pub client_credentials: Option<ClientCredentialsOAuthFlow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub implicit: Option<ImplicitOAuthFlow>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<PasswordOAuthFlow>,
#[serde(skip_serializing_if = "Option::is_none", rename = "deviceCode")]
pub device_code: Option<DeviceCodeOAuthFlow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AuthorizationCodeOAuthFlow {
#[serde(rename = "authorizationUrl")]
pub authorization_url: String,
#[serde(rename = "tokenUrl")]
pub token_url: String,
#[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
pub scopes: HashMap<String, String>,
#[serde(rename = "pkceRequired", skip_serializing_if = "Option::is_none")]
pub pkce_required: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ClientCredentialsOAuthFlow {
#[serde(rename = "tokenUrl")]
pub token_url: String,
#[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
pub scopes: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ImplicitOAuthFlow {
#[serde(rename = "authorizationUrl", skip_serializing_if = "Option::is_none")]
pub authorization_url: Option<String>,
#[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(default)]
pub scopes: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PasswordOAuthFlow {
#[serde(rename = "tokenUrl", skip_serializing_if = "Option::is_none")]
pub token_url: Option<String>,
#[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
#[serde(default)]
pub scopes: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DeviceCodeOAuthFlow {
#[serde(rename = "deviceAuthorizationUrl")]
pub device_authorization_url: String,
#[serde(rename = "tokenUrl")]
pub token_url: String,
#[serde(rename = "refreshUrl", skip_serializing_if = "Option::is_none")]
pub refresh_url: Option<String>,
pub scopes: HashMap<String, String>,
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_card() -> AgentCard {
AgentCard {
name: "Test Agent".to_string(),
description: "A test agent".to_string(),
version: "1.0.0".to_string(),
supported_interfaces: vec![AgentInterface {
url: "https://example.com/a2a".to_string(),
protocol_binding: "JSONRPC".to_string(),
tenant: None,
protocol_version: "0.3".to_string(),
}],
capabilities: AgentCapabilities::default(),
skills: vec![],
default_input_modes: vec!["text/plain".to_string()],
default_output_modes: vec!["text/plain".to_string()],
provider: None,
security_schemes: None,
security_requirements: None,
documentation_url: None,
icon_url: None,
signatures: None,
}
}
#[test]
fn agent_card_roundtrip() {
let card = minimal_card();
let json = serde_json::to_string(&card).unwrap();
let back: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, card.name);
assert_eq!(back.version, card.version);
}
#[test]
fn agent_card_json_uses_camel_case() {
let card = minimal_card();
let json = serde_json::to_string(&card).unwrap();
assert!(json.contains("supportedInterfaces"));
assert!(json.contains("defaultInputModes"));
assert!(json.contains("defaultOutputModes"));
}
#[test]
fn optional_fields_omitted_when_none() {
let card = minimal_card();
let json = serde_json::to_string(&card).unwrap();
assert!(!json.contains("provider"));
assert!(!json.contains("securitySchemes"));
assert!(!json.contains("documentationUrl"));
assert!(!json.contains("iconUrl"));
assert!(!json.contains("signatures"));
}
#[test]
fn agent_card_with_skill_roundtrip() {
let mut card = minimal_card();
card.skills = vec![AgentSkill {
id: "skill-1".to_string(),
name: "My Skill".to_string(),
description: "Does something".to_string(),
tags: vec!["tag1".to_string()],
examples: Some(vec!["example prompt".to_string()]),
input_modes: None,
output_modes: None,
security_requirements: None,
}];
let json = serde_json::to_string(&card).unwrap();
let back: AgentCard = serde_json::from_str(&json).unwrap();
assert_eq!(back.skills.len(), 1);
assert_eq!(back.skills[0].id, "skill-1");
}
#[test]
fn capabilities_default_all_none() {
let cap = AgentCapabilities::default();
let json = serde_json::to_string(&cap).unwrap();
assert!(!json.contains("streaming"));
assert!(!json.contains("pushNotifications"));
assert!(!json.contains("extendedAgentCard"));
}
#[test]
fn capabilities_with_streaming_roundtrip() {
let cap = AgentCapabilities {
streaming: Some(true),
push_notifications: Some(false),
extended_agent_card: None,
extensions: None,
};
let json = serde_json::to_string(&cap).unwrap();
let back: AgentCapabilities = serde_json::from_str(&json).unwrap();
assert_eq!(back.streaming, Some(true));
assert_eq!(back.push_notifications, Some(false));
}
#[test]
fn api_key_security_scheme_roundtrip() {
let scheme = SecurityScheme {
api_key: Some(ApiKeySecurityScheme {
name: "X-Api-Key".to_string(),
location: "header".to_string(),
description: None,
}),
http_auth: None,
oauth2: None,
open_id_connect: None,
mtls: None,
};
let json = serde_json::to_string(&scheme).unwrap();
let back: SecurityScheme = serde_json::from_str(&json).unwrap();
assert_eq!(back, scheme);
assert!(json.contains("apiKeySecurityScheme"));
}
#[test]
fn oauth2_authorization_code_flow_roundtrip() {
let scheme = SecurityScheme {
api_key: None,
http_auth: None,
oauth2: Some(OAuth2SecurityScheme {
flows: OAuthFlows {
authorization_code: Some(AuthorizationCodeOAuthFlow {
authorization_url: "https://auth.example.com/authorize".to_string(),
token_url: "https://auth.example.com/token".to_string(),
refresh_url: None,
scopes: [("read".to_string(), "Read access".to_string())]
.into_iter()
.collect(),
pkce_required: Some(true),
}),
client_credentials: None,
implicit: None,
password: None,
device_code: None,
},
description: None,
oauth2_metadata_url: None,
}),
open_id_connect: None,
mtls: None,
};
let json = serde_json::to_string(&scheme).unwrap();
let back: SecurityScheme = serde_json::from_str(&json).unwrap();
assert_eq!(back, scheme);
}
#[test]
fn agent_interface_json_uses_camel_case() {
let iface = AgentInterface {
url: "https://example.com".to_string(),
protocol_binding: "JSONRPC".to_string(),
tenant: None,
protocol_version: "0.3".to_string(),
};
let json = serde_json::to_string(&iface).unwrap();
assert!(json.contains("protocolBinding"));
assert!(json.contains("protocolVersion"));
assert!(!json.contains("protocol_binding"));
}
#[test]
fn agent_provider_roundtrip() {
let p = AgentProvider {
url: "https://acme.io".to_string(),
organization: "ACME Corp".to_string(),
};
let json = serde_json::to_string(&p).unwrap();
let back: AgentProvider = serde_json::from_str(&json).unwrap();
assert_eq!(back.organization, "ACME Corp");
}
}