|
1 | 1 | // Copyright AGNTCY Contributors (https://github.com/agntcy) |
2 | 2 | // SPDX-License-Identifier: Apache-2.0 |
3 | | -use serde::{Deserialize, Deserializer, Serialize}; |
| 3 | +use serde::{Deserialize, Deserializer, Serialize, Serializer}; |
4 | 4 | use serde_json::Value; |
5 | 5 | use std::collections::HashMap; |
6 | 6 |
|
7 | | -use crate::types::{ProtocolVersion, TransportProtocol}; |
| 7 | +use crate::types::{ProtocolVersion, TRANSPORT_PROTOCOL_GRPC, TransportProtocol}; |
8 | 8 |
|
9 | 9 | // --------------------------------------------------------------------------- |
10 | 10 | // AgentCard |
@@ -56,26 +56,77 @@ where |
56 | 56 | // --------------------------------------------------------------------------- |
57 | 57 |
|
58 | 58 | /// A URL + protocol binding combination for reaching the agent. |
59 | | -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
60 | | -#[serde(rename_all = "camelCase")] |
| 59 | +#[derive(Debug, Clone, PartialEq)] |
61 | 60 | pub struct AgentInterface { |
62 | 61 | pub url: String, |
63 | 62 | pub protocol_binding: TransportProtocol, |
64 | 63 | pub protocol_version: ProtocolVersion, |
65 | | - |
66 | | - #[serde(default, skip_serializing_if = "Option::is_none")] |
67 | 64 | pub tenant: Option<String>, |
68 | 65 | } |
69 | 66 |
|
| 67 | +#[derive(Deserialize)] |
| 68 | +#[serde(rename_all = "camelCase")] |
| 69 | +struct AgentInterfaceSerde { |
| 70 | + url: String, |
| 71 | + protocol_binding: TransportProtocol, |
| 72 | + protocol_version: ProtocolVersion, |
| 73 | + #[serde(default)] |
| 74 | + tenant: Option<String>, |
| 75 | +} |
| 76 | + |
| 77 | +fn normalize_agent_interface_url(url: String, protocol_binding: &str) -> String { |
| 78 | + if protocol_binding.eq_ignore_ascii_case(TRANSPORT_PROTOCOL_GRPC) { |
| 79 | + if let Some(stripped) = url.strip_prefix("http://") { |
| 80 | + return stripped.to_string(); |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + url |
| 85 | +} |
| 86 | + |
70 | 87 | impl AgentInterface { |
71 | 88 | pub fn new(url: impl Into<String>, protocol_binding: impl Into<String>) -> Self { |
| 89 | + let protocol_binding = protocol_binding.into(); |
72 | 90 | AgentInterface { |
73 | | - url: url.into(), |
74 | | - protocol_binding: protocol_binding.into(), |
| 91 | + url: normalize_agent_interface_url(url.into(), &protocol_binding), |
| 92 | + protocol_binding, |
75 | 93 | protocol_version: crate::VERSION.to_string(), |
76 | 94 | tenant: None, |
77 | 95 | } |
78 | 96 | } |
| 97 | + |
| 98 | + pub fn wire_url(&self) -> String { |
| 99 | + normalize_agent_interface_url(self.url.clone(), &self.protocol_binding) |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +impl Serialize for AgentInterface { |
| 104 | + fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { |
| 105 | + use serde::ser::SerializeStruct; |
| 106 | + |
| 107 | + let mut state = serializer |
| 108 | + .serialize_struct("AgentInterface", if self.tenant.is_some() { 4 } else { 3 })?; |
| 109 | + state.serialize_field("url", &self.wire_url())?; |
| 110 | + state.serialize_field("protocolBinding", &self.protocol_binding)?; |
| 111 | + state.serialize_field("protocolVersion", &self.protocol_version)?; |
| 112 | + if let Some(tenant) = &self.tenant { |
| 113 | + state.serialize_field("tenant", tenant)?; |
| 114 | + } |
| 115 | + state.end() |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +impl<'de> Deserialize<'de> for AgentInterface { |
| 120 | + fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { |
| 121 | + let raw = AgentInterfaceSerde::deserialize(deserializer)?; |
| 122 | + |
| 123 | + Ok(Self { |
| 124 | + url: normalize_agent_interface_url(raw.url, &raw.protocol_binding), |
| 125 | + protocol_binding: raw.protocol_binding, |
| 126 | + protocol_version: raw.protocol_version, |
| 127 | + tenant: raw.tenant, |
| 128 | + }) |
| 129 | + } |
79 | 130 | } |
80 | 131 |
|
81 | 132 | // --------------------------------------------------------------------------- |
@@ -445,6 +496,81 @@ mod tests { |
445 | 496 | assert!(!iface.protocol_version.is_empty()); |
446 | 497 | } |
447 | 498 |
|
| 499 | + #[test] |
| 500 | + fn test_agent_interface_new_normalizes_grpc_http_scheme() { |
| 501 | + let iface = AgentInterface::new("http://localhost:50051", TRANSPORT_PROTOCOL_GRPC); |
| 502 | + assert_eq!(iface.url, "localhost:50051"); |
| 503 | + assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC); |
| 504 | + } |
| 505 | + |
| 506 | + #[test] |
| 507 | + fn test_agent_interface_new_preserves_grpc_https_scheme() { |
| 508 | + let iface = AgentInterface::new("https://localhost:50051", TRANSPORT_PROTOCOL_GRPC); |
| 509 | + assert_eq!(iface.url, "https://localhost:50051"); |
| 510 | + assert_eq!(iface.protocol_binding, TRANSPORT_PROTOCOL_GRPC); |
| 511 | + } |
| 512 | + |
| 513 | + #[test] |
| 514 | + fn test_agent_interface_serde_normalizes_grpc_http_scheme() { |
| 515 | + let iface = AgentInterface { |
| 516 | + url: "http://localhost:50051".to_string(), |
| 517 | + protocol_binding: TRANSPORT_PROTOCOL_GRPC.to_string(), |
| 518 | + protocol_version: crate::VERSION.to_string(), |
| 519 | + tenant: Some("tenant-a".to_string()), |
| 520 | + }; |
| 521 | + |
| 522 | + let json = serde_json::to_string(&iface).unwrap(); |
| 523 | + assert!(json.contains("\"url\":\"localhost:50051\"")); |
| 524 | + |
| 525 | + let back: AgentInterface = serde_json::from_str(&json).unwrap(); |
| 526 | + assert_eq!(back.url, "localhost:50051"); |
| 527 | + assert_eq!(back.protocol_binding, TRANSPORT_PROTOCOL_GRPC); |
| 528 | + assert_eq!(back.tenant.as_deref(), Some("tenant-a")); |
| 529 | + } |
| 530 | + |
| 531 | + #[test] |
| 532 | + fn test_agent_card_deserialize_null_skills_as_default() { |
| 533 | + let json = serde_json::json!({ |
| 534 | + "name": "Test Agent", |
| 535 | + "description": "A test agent", |
| 536 | + "version": "1.0.0", |
| 537 | + "supportedInterfaces": [ |
| 538 | + { |
| 539 | + "url": "http://localhost:3000", |
| 540 | + "protocolBinding": "JSONRPC", |
| 541 | + "protocolVersion": crate::VERSION |
| 542 | + } |
| 543 | + ], |
| 544 | + "capabilities": {}, |
| 545 | + "defaultInputModes": ["text/plain"], |
| 546 | + "defaultOutputModes": ["text/plain"], |
| 547 | + "skills": null |
| 548 | + }); |
| 549 | + |
| 550 | + let card: AgentCard = serde_json::from_value(json).unwrap(); |
| 551 | + assert!(card.skills.is_empty()); |
| 552 | + } |
| 553 | + |
| 554 | + #[test] |
| 555 | + fn test_security_scheme_deserialize_unknown_variant_errors() { |
| 556 | + let err = serde_json::from_value::<SecurityScheme>(serde_json::json!({ |
| 557 | + "unknown": {"value": true} |
| 558 | + })) |
| 559 | + .unwrap_err(); |
| 560 | + |
| 561 | + assert!(err.to_string().contains("unknown security scheme variant")); |
| 562 | + } |
| 563 | + |
| 564 | + #[test] |
| 565 | + fn test_oauth_flows_deserialize_unknown_variant_errors() { |
| 566 | + let err = serde_json::from_value::<OAuthFlows>(serde_json::json!({ |
| 567 | + "unknown": {"tokenUrl": "https://example.com/token"} |
| 568 | + })) |
| 569 | + .unwrap_err(); |
| 570 | + |
| 571 | + assert!(err.to_string().contains("unknown OAuth flow variant")); |
| 572 | + } |
| 573 | + |
448 | 574 | #[test] |
449 | 575 | fn test_security_scheme_apikey_serde() { |
450 | 576 | let ss = SecurityScheme::ApiKey(ApiKeySecurityScheme { |
|
0 commit comments