1use std::collections::BTreeMap;
10use std::sync::Arc;
11
12use url::Url;
13
14use crate::domain::a2a::agent_card::error::AgentCardError;
15use crate::domain::a2a::agent_card::security_requirement::SecurityRequirement;
16use crate::domain::a2a::agent_card::security_scheme::SecurityScheme;
17use crate::domain::a2a::agent_card::types::{
18 AgentCapabilities, AgentCard, AgentExtension, AgentInterface, AgentProvider, AgentSkill,
19};
20
21#[derive(Debug, Default)]
28pub struct AgentCardBuilder {
29 name: Option<Arc<str>>,
30 description: Option<Arc<str>>,
31 version: Option<Arc<str>>,
32 supported_interfaces: Vec<AgentInterface>,
33 provider: Option<AgentProvider>,
34 documentation_url: Option<Url>,
35 icon_url: Option<Url>,
36 capabilities: AgentCapabilities,
37 default_input_modes: Vec<Arc<str>>,
38 default_output_modes: Vec<Arc<str>>,
39 skills: Vec<AgentSkill>,
40 security_schemes: BTreeMap<Arc<str>, SecurityScheme>,
41 security_requirements: Vec<SecurityRequirement>,
42}
43
44impl AgentCardBuilder {
45 #[must_use]
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 #[must_use]
53 pub fn with_name(mut self, name: impl Into<Arc<str>>) -> Self {
54 self.name = Some(name.into());
55 self
56 }
57
58 #[must_use]
60 pub fn with_description(mut self, description: impl Into<Arc<str>>) -> Self {
61 self.description = Some(description.into());
62 self
63 }
64
65 #[must_use]
67 pub fn with_version(mut self, version: impl Into<Arc<str>>) -> Self {
68 self.version = Some(version.into());
69 self
70 }
71
72 #[must_use]
76 pub fn with_interface(mut self, interface: AgentInterface) -> Self {
77 self.supported_interfaces.push(interface);
78 self
79 }
80
81 #[must_use]
85 pub fn with_provider(mut self, provider: AgentProvider) -> Self {
86 self.provider = Some(provider);
87 self
88 }
89
90 #[must_use]
94 pub fn with_documentation_url(mut self, url: Url) -> Self {
95 self.documentation_url = Some(url);
96 self
97 }
98
99 #[must_use]
104 pub fn with_icon_url(mut self, url: Url) -> Self {
105 self.icon_url = Some(url);
106 self
107 }
108
109 #[must_use]
111 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
112 self.capabilities = capabilities;
113 self
114 }
115
116 #[must_use]
122 pub fn with_extension(mut self, extension: AgentExtension) -> Self {
123 self.capabilities.extensions.push(extension);
124 self
125 }
126
127 #[must_use]
134 pub fn with_security_scheme(
135 mut self,
136 name: impl Into<Arc<str>>,
137 scheme: SecurityScheme,
138 ) -> Self {
139 self.security_schemes.insert(name.into(), scheme);
140 self
141 }
142
143 #[must_use]
148 pub fn with_security_requirement(mut self, requirement: SecurityRequirement) -> Self {
149 self.security_requirements.push(requirement);
150 self
151 }
152
153 #[must_use]
155 pub fn with_input_mode(mut self, mode: impl Into<Arc<str>>) -> Self {
156 self.default_input_modes.push(mode.into());
157 self
158 }
159
160 #[must_use]
162 pub fn with_output_mode(mut self, mode: impl Into<Arc<str>>) -> Self {
163 self.default_output_modes.push(mode.into());
164 self
165 }
166
167 #[must_use]
169 pub fn with_skill(mut self, skill: AgentSkill) -> Self {
170 self.skills.push(skill);
171 self
172 }
173
174 pub fn build(self) -> Result<AgentCard, AgentCardError> {
182 let name = self.name.as_ref().ok_or(AgentCardError::EmptyName)?.clone();
183 if name.is_empty() {
184 return Err(AgentCardError::EmptyName);
185 }
186 let description = self
187 .description
188 .as_ref()
189 .ok_or(AgentCardError::EmptyDescription)?
190 .clone();
191 if description.is_empty() {
192 return Err(AgentCardError::EmptyDescription);
193 }
194 let version = self
195 .version
196 .as_ref()
197 .ok_or(AgentCardError::EmptyVersion)?
198 .clone();
199 if version.is_empty() {
200 return Err(AgentCardError::EmptyVersion);
201 }
202 self.validate_top_level()?;
203 self.validate_skills()?;
204 self.validate_provider_extensions_and_security()?;
205 Ok(AgentCard {
206 name,
207 description,
208 version,
209 supported_interfaces: self.supported_interfaces,
210 provider: self.provider,
211 documentation_url: self.documentation_url,
212 icon_url: self.icon_url,
213 capabilities: self.capabilities,
214 default_input_modes: self.default_input_modes,
215 default_output_modes: self.default_output_modes,
216 skills: self.skills,
217 security_schemes: self.security_schemes,
218 security_requirements: self.security_requirements,
219 })
220 }
221
222 const fn validate_top_level(&self) -> Result<(), AgentCardError> {
226 if self.supported_interfaces.is_empty() {
227 return Err(AgentCardError::NoSupportedInterfaces);
228 }
229 if self.default_input_modes.is_empty() {
230 return Err(AgentCardError::NoInputModes);
231 }
232 if self.default_output_modes.is_empty() {
233 return Err(AgentCardError::NoOutputModes);
234 }
235 Ok(())
236 }
237
238 fn validate_skills(&self) -> Result<(), AgentCardError> {
241 for (index, skill) in self.skills.iter().enumerate() {
242 if skill.id.is_empty() {
243 return Err(AgentCardError::EmptySkillField { index, field: "id" });
244 }
245 if skill.name.is_empty() {
246 return Err(AgentCardError::EmptySkillField {
247 index,
248 field: "name",
249 });
250 }
251 if skill.description.is_empty() {
252 return Err(AgentCardError::EmptySkillField {
253 index,
254 field: "description",
255 });
256 }
257 }
258 Ok(())
259 }
260
261 fn validate_provider_extensions_and_security(&self) -> Result<(), AgentCardError> {
270 if let Some(provider) = &self.provider
271 && provider.organization.is_empty()
272 {
273 return Err(AgentCardError::EmptyProviderOrganization);
274 }
275 for (index, extension) in self.capabilities.extensions.iter().enumerate() {
276 if extension.uri.is_empty() {
277 return Err(AgentCardError::EmptyExtensionField {
278 index,
279 field: "uri",
280 });
281 }
282 if extension.description.is_empty() {
283 return Err(AgentCardError::EmptyExtensionField {
284 index,
285 field: "description",
286 });
287 }
288 }
289 for (name, scheme) in &self.security_schemes {
290 if name.is_empty() {
291 return Err(AgentCardError::EmptySecuritySchemeName);
292 }
293 match scheme {
294 SecurityScheme::ApiKey(api_key) => {
295 if api_key.name.is_empty() {
296 return Err(AgentCardError::EmptyAPIKeyParameterName {
297 name: Arc::clone(name),
298 });
299 }
300 }
301 SecurityScheme::Http(http) => {
302 if http.scheme.is_empty() {
303 return Err(AgentCardError::EmptyHTTPAuthScheme {
304 name: Arc::clone(name),
305 });
306 }
307 }
308 SecurityScheme::OAuth2(oauth2) => {
309 if !oauth2.flows.has_any() {
310 return Err(AgentCardError::EmptyOAuthFlows {
311 name: Arc::clone(name),
312 });
313 }
314 }
315 SecurityScheme::OpenIdConnect(_) | SecurityScheme::MutualTls(_) => {
316 }
324 }
325 }
326 Ok(())
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 #![expect(
333 clippy::expect_used,
334 reason = "tests assert on fallible calls with named failure messages; propagating errors would replace a clear failure with noise"
335 )]
336
337 use std::sync::Arc;
338
339 use super::{AgentCard, AgentCardBuilder};
340 use crate::domain::a2a::A2aVersion;
341 use crate::domain::a2a::agent_card::error::AgentCardError;
342 use crate::domain::a2a::agent_card::types::{AgentSkill, JSONRPC};
343
344 type TestError = Box<dyn std::error::Error>;
345
346 fn sample_interface() -> Result<crate::domain::a2a::agent_card::types::AgentInterface, TestError>
347 {
348 Ok(crate::domain::a2a::agent_card::types::AgentInterface {
349 url: url::Url::parse("https://example.com/a2a/v1")?,
350 protocol_binding: JSONRPC.clone(),
351 protocol_version: A2aVersion::V1_0,
352 tenant: None,
353 })
354 }
355
356 fn sample_skill() -> AgentSkill {
357 AgentSkill {
358 id: Arc::from("research"),
359 name: Arc::from("Research"),
360 description: Arc::from("Look things up."),
361 tags: vec![],
362 }
363 }
364
365 fn sample_card() -> Result<AgentCard, TestError> {
366 Ok(AgentCardBuilder::new()
367 .with_name("Research Agent")
368 .with_description("Looks things up on the web.")
369 .with_version("1.0.0")
370 .with_interface(sample_interface()?)
371 .with_capabilities(crate::domain::a2a::agent_card::types::AgentCapabilities::default())
372 .with_input_mode("text/plain")
373 .with_output_mode("text/plain")
374 .with_skill(sample_skill())
375 .build()?)
376 }
377
378 #[test]
381 fn empty_name_is_rejected() -> Result<(), TestError> {
382 let err = AgentCardBuilder::new()
383 .with_name("")
384 .with_description("d")
385 .with_version("1.0.0")
386 .with_interface(sample_interface()?)
387 .with_input_mode("text/plain")
388 .with_output_mode("text/plain")
389 .build()
390 .expect_err("an empty name must be refused");
391 assert_eq!(err, AgentCardError::EmptyName);
392 Ok(())
393 }
394
395 #[test]
396 fn missing_name_is_rejected() -> Result<(), TestError> {
397 let err = AgentCardBuilder::new()
398 .with_description("d")
399 .with_version("1.0.0")
400 .with_interface(sample_interface()?)
401 .with_input_mode("text/plain")
402 .with_output_mode("text/plain")
403 .build()
404 .expect_err("a missing name must be refused");
405 assert_eq!(err, AgentCardError::EmptyName);
406 Ok(())
407 }
408
409 #[test]
410 fn empty_description_is_rejected() -> Result<(), TestError> {
411 let err = AgentCardBuilder::new()
412 .with_name("n")
413 .with_description("")
414 .with_version("1.0.0")
415 .with_interface(sample_interface()?)
416 .with_input_mode("text/plain")
417 .with_output_mode("text/plain")
418 .build()
419 .expect_err("an empty description must be refused");
420 assert_eq!(err, AgentCardError::EmptyDescription);
421 Ok(())
422 }
423
424 #[test]
425 fn empty_version_is_rejected() -> Result<(), TestError> {
426 let err = AgentCardBuilder::new()
427 .with_name("n")
428 .with_description("d")
429 .with_version("")
430 .with_interface(sample_interface()?)
431 .with_input_mode("text/plain")
432 .with_output_mode("text/plain")
433 .build()
434 .expect_err("an empty version must be refused");
435 assert_eq!(err, AgentCardError::EmptyVersion);
436 Ok(())
437 }
438
439 #[test]
440 fn no_supported_interfaces_is_rejected() {
441 let err = AgentCardBuilder::new()
442 .with_name("n")
443 .with_description("d")
444 .with_version("1.0.0")
445 .with_input_mode("text/plain")
446 .with_output_mode("text/plain")
447 .build()
448 .expect_err("zero interfaces must be refused");
449 assert_eq!(err, AgentCardError::NoSupportedInterfaces);
450 }
451
452 #[test]
453 fn no_input_modes_is_rejected() -> Result<(), TestError> {
454 let err = AgentCardBuilder::new()
455 .with_name("n")
456 .with_description("d")
457 .with_version("1.0.0")
458 .with_interface(sample_interface()?)
459 .with_output_mode("text/plain")
460 .build()
461 .expect_err("zero input modes must be refused");
462 assert_eq!(err, AgentCardError::NoInputModes);
463 Ok(())
464 }
465
466 #[test]
467 fn no_output_modes_is_rejected() -> Result<(), TestError> {
468 let err = AgentCardBuilder::new()
469 .with_name("n")
470 .with_description("d")
471 .with_version("1.0.0")
472 .with_interface(sample_interface()?)
473 .with_input_mode("text/plain")
474 .build()
475 .expect_err("zero output modes must be refused");
476 assert_eq!(err, AgentCardError::NoOutputModes);
477 Ok(())
478 }
479
480 #[test]
481 fn empty_skill_id_is_rejected() -> Result<(), TestError> {
482 let bad = AgentSkill {
483 id: Arc::from(""),
484 name: Arc::from("Research"),
485 description: Arc::from("Look things up."),
486 tags: vec![],
487 };
488 let err = AgentCardBuilder::new()
489 .with_name("n")
490 .with_description("d")
491 .with_version("1.0.0")
492 .with_interface(sample_interface()?)
493 .with_input_mode("text/plain")
494 .with_output_mode("text/plain")
495 .with_skill(bad)
496 .build()
497 .expect_err("an empty skill id must be refused");
498 assert_eq!(
499 err,
500 AgentCardError::EmptySkillField {
501 index: 0,
502 field: "id"
503 }
504 );
505 Ok(())
506 }
507
508 #[test]
509 fn valid_card_builds() -> Result<(), TestError> {
510 let card = sample_card()?;
511 assert_eq!(card.name.as_ref(), "Research Agent");
512 assert_eq!(card.supported_interfaces.len(), 1);
513 assert_eq!(card.skills.len(), 1);
514 Ok(())
515 }
516
517 #[test]
520 fn valid_provider_builds() -> Result<(), TestError> {
521 let card = AgentCardBuilder::new()
522 .with_name("n")
523 .with_description("d")
524 .with_version("1.0.0")
525 .with_interface(sample_interface()?)
526 .with_provider(super::AgentProvider {
527 url: url::Url::parse("https://example.com")?,
528 organization: Arc::from("Example Co"),
529 })
530 .with_input_mode("text/plain")
531 .with_output_mode("text/plain")
532 .build()?;
533 let provider = card
534 .provider
535 .as_ref()
536 .expect("a card built with_provider must carry the provider");
537 assert_eq!(provider.organization.as_ref(), "Example Co");
538 assert_eq!(provider.url.as_str(), "https://example.com/");
539 Ok(())
540 }
541
542 #[test]
543 fn empty_provider_organization_is_rejected() -> Result<(), TestError> {
544 let err = AgentCardBuilder::new()
545 .with_name("n")
546 .with_description("d")
547 .with_version("1.0.0")
548 .with_interface(sample_interface()?)
549 .with_provider(super::AgentProvider {
550 url: url::Url::parse("https://example.com")?,
551 organization: Arc::from(""),
552 })
553 .with_input_mode("text/plain")
554 .with_output_mode("text/plain")
555 .build()
556 .expect_err("an empty provider organization must be refused");
557 assert_eq!(err, AgentCardError::EmptyProviderOrganization);
558 Ok(())
559 }
560
561 #[test]
562 fn unset_provider_carries_none() -> Result<(), TestError> {
563 let card = sample_card()?;
568 assert!(card.provider.is_none());
569 let value = serde_json::to_value(&card)?;
570 let obj = value
571 .as_object()
572 .expect("the card must serialise to a JSON object");
573 assert!(!obj.contains_key("provider"));
574 Ok(())
575 }
576
577 #[test]
578 fn documentation_url_round_trips() -> Result<(), TestError> {
579 let url = url::Url::parse("https://example.com/docs")?;
580 let card = AgentCardBuilder::new()
581 .with_name("n")
582 .with_description("d")
583 .with_version("1.0.0")
584 .with_interface(sample_interface()?)
585 .with_documentation_url(url.clone())
586 .with_input_mode("text/plain")
587 .with_output_mode("text/plain")
588 .build()?;
589 assert_eq!(card.documentation_url.as_ref(), Some(&url));
590 let json = serde_json::to_string(&card)?;
591 let parsed: AgentCard = serde_json::from_str(&json)?;
592 assert_eq!(parsed, card);
593 Ok(())
594 }
595
596 #[test]
597 fn icon_url_round_trips() -> Result<(), TestError> {
598 let url = url::Url::parse("https://example.com/icon.png")?;
599 let card = AgentCardBuilder::new()
600 .with_name("n")
601 .with_description("d")
602 .with_version("1.0.0")
603 .with_interface(sample_interface()?)
604 .with_icon_url(url.clone())
605 .with_input_mode("text/plain")
606 .with_output_mode("text/plain")
607 .build()?;
608 assert_eq!(card.icon_url.as_ref(), Some(&url));
609 let json = serde_json::to_string(&card)?;
610 let parsed: AgentCard = serde_json::from_str(&json)?;
611 assert_eq!(parsed, card);
612 Ok(())
613 }
614
615 #[test]
618 fn valid_extension_builds() -> Result<(), TestError> {
619 let card = AgentCardBuilder::new()
620 .with_name("n")
621 .with_description("d")
622 .with_version("1.0.0")
623 .with_interface(sample_interface()?)
624 .with_extension(super::AgentExtension {
625 uri: Arc::from("https://example.com/extension/audio-input"),
626 description: Arc::from("Accepts audio input on the SendMessage payload."),
627 required: false,
628 params: None,
629 })
630 .with_input_mode("text/plain")
631 .with_output_mode("text/plain")
632 .build()?;
633 assert_eq!(card.capabilities.extensions.len(), 1);
634 let ext = card
635 .capabilities
636 .extensions
637 .first()
638 .expect("the card carries exactly one extension");
639 assert_eq!(
640 ext.uri.as_ref(),
641 "https://example.com/extension/audio-input"
642 );
643 assert!(!ext.required);
644 assert!(ext.params.is_none());
645 Ok(())
646 }
647
648 #[test]
649 fn empty_extension_uri_is_rejected() -> Result<(), TestError> {
650 let err = AgentCardBuilder::new()
651 .with_name("n")
652 .with_description("d")
653 .with_version("1.0.0")
654 .with_interface(sample_interface()?)
655 .with_extension(super::AgentExtension {
656 uri: Arc::from(""),
657 description: Arc::from("d"),
658 required: false,
659 params: None,
660 })
661 .with_input_mode("text/plain")
662 .with_output_mode("text/plain")
663 .build()
664 .expect_err("an empty extension uri must be refused");
665 assert_eq!(
666 err,
667 AgentCardError::EmptyExtensionField {
668 index: 0,
669 field: "uri"
670 }
671 );
672 Ok(())
673 }
674
675 #[test]
676 fn empty_extension_description_is_rejected() -> Result<(), TestError> {
677 let err = AgentCardBuilder::new()
678 .with_name("n")
679 .with_description("d")
680 .with_version("1.0.0")
681 .with_interface(sample_interface()?)
682 .with_extension(super::AgentExtension {
683 uri: Arc::from("https://example.com/ext"),
684 description: Arc::from(""),
685 required: false,
686 params: None,
687 })
688 .with_input_mode("text/plain")
689 .with_output_mode("text/plain")
690 .build()
691 .expect_err("an empty extension description must be refused");
692 assert_eq!(
693 err,
694 AgentCardError::EmptyExtensionField {
695 index: 0,
696 field: "description"
697 }
698 );
699 Ok(())
700 }
701
702 #[test]
703 fn extension_params_round_trip_as_json_object() -> Result<(), TestError> {
704 let mut params = serde_json::Map::new();
709 params.insert(
710 "mimeType".to_owned(),
711 serde_json::Value::String("audio/wav".to_owned()),
712 );
713 let card = AgentCardBuilder::new()
714 .with_name("n")
715 .with_description("d")
716 .with_version("1.0.0")
717 .with_interface(sample_interface()?)
718 .with_extension(super::AgentExtension {
719 uri: Arc::from("https://example.com/ext"),
720 description: Arc::from("d"),
721 required: false,
722 params: Some(params.clone()),
723 })
724 .with_input_mode("text/plain")
725 .with_output_mode("text/plain")
726 .build()?;
727 let value = serde_json::to_value(
728 card.capabilities
729 .extensions
730 .first()
731 .expect("the card carries exactly one extension"),
732 )?;
733 let obj = value
734 .as_object()
735 .expect("extension must serialise to a JSON object");
736 let mime = obj
737 .get("params")
738 .and_then(serde_json::Value::as_object)
739 .and_then(|p| p.get("mimeType"))
740 .and_then(serde_json::Value::as_str)
741 .map(str::to_owned);
742 assert_eq!(mime.as_deref(), Some("audio/wav"));
743 Ok(())
744 }
745
746 #[test]
749 fn api_key_security_scheme_round_trips() -> Result<(), TestError> {
750 use super::super::security_scheme::{APIKeyLocation, APIKeySecurityScheme, SecurityScheme};
751
752 let card = AgentCardBuilder::new()
753 .with_name("n")
754 .with_description("d")
755 .with_version("1.0.0")
756 .with_interface(sample_interface()?)
757 .with_security_scheme(
758 "publicRead",
759 SecurityScheme::ApiKey(APIKeySecurityScheme {
760 description: Some(Arc::from("Read-only API key.")),
761 in_: APIKeyLocation::Header,
762 name: Arc::from("X-API-Key"),
763 }),
764 )
765 .with_input_mode("text/plain")
766 .with_output_mode("text/plain")
767 .build()?;
768 let scheme = card
769 .security_schemes
770 .get("publicRead")
771 .expect("the registered scheme must be present in the map");
772 let json = serde_json::to_value(scheme)?;
773 let obj = json
774 .as_object()
775 .expect("the scheme must serialise to a JSON object");
776 assert_eq!(
777 obj.get("type").and_then(serde_json::Value::as_str),
778 Some("apiKey")
779 );
780 assert_eq!(
781 obj.get("in").and_then(serde_json::Value::as_str),
782 Some("header")
783 );
784 assert_eq!(
785 obj.get("name").and_then(serde_json::Value::as_str),
786 Some("X-API-Key")
787 );
788 Ok(())
789 }
790
791 #[test]
792 fn http_security_scheme_round_trips() -> Result<(), TestError> {
793 use super::super::security_scheme::{HTTPAuthSecurityScheme, SecurityScheme};
794
795 let card = AgentCardBuilder::new()
796 .with_name("n")
797 .with_description("d")
798 .with_version("1.0.0")
799 .with_interface(sample_interface()?)
800 .with_security_scheme(
801 "bearerAuth",
802 SecurityScheme::Http(HTTPAuthSecurityScheme {
803 description: None,
804 scheme: Arc::from("Bearer"),
805 bearer_format: Some(Arc::from("JWT")),
806 }),
807 )
808 .with_input_mode("text/plain")
809 .with_output_mode("text/plain")
810 .build()?;
811 let scheme = card
812 .security_schemes
813 .get("bearerAuth")
814 .expect("the registered scheme must be present in the map");
815 let json = serde_json::to_value(scheme)?;
816 let obj = json
817 .as_object()
818 .expect("the scheme must serialise to a JSON object");
819 assert_eq!(
820 obj.get("type").and_then(serde_json::Value::as_str),
821 Some("http")
822 );
823 assert_eq!(
824 obj.get("scheme").and_then(serde_json::Value::as_str),
825 Some("Bearer")
826 );
827 assert_eq!(
828 obj.get("bearerFormat").and_then(serde_json::Value::as_str),
829 Some("JWT")
830 );
831 Ok(())
832 }
833
834 #[test]
835 fn empty_security_scheme_name_is_rejected() -> Result<(), TestError> {
836 use super::super::security_scheme::{APIKeyLocation, APIKeySecurityScheme, SecurityScheme};
837
838 let err = AgentCardBuilder::new()
839 .with_name("n")
840 .with_description("d")
841 .with_version("1.0.0")
842 .with_interface(sample_interface()?)
843 .with_security_scheme(
844 "",
845 SecurityScheme::ApiKey(APIKeySecurityScheme {
846 description: None,
847 in_: APIKeyLocation::Header,
848 name: Arc::from("X-API-Key"),
849 }),
850 )
851 .with_input_mode("text/plain")
852 .with_output_mode("text/plain")
853 .build()
854 .expect_err("an empty scheme name must be refused");
855 assert_eq!(err, AgentCardError::EmptySecuritySchemeName);
856 Ok(())
857 }
858
859 #[test]
860 fn empty_api_key_parameter_name_is_rejected() -> Result<(), TestError> {
861 use super::super::security_scheme::{APIKeyLocation, APIKeySecurityScheme, SecurityScheme};
862
863 let err = AgentCardBuilder::new()
864 .with_name("n")
865 .with_description("d")
866 .with_version("1.0.0")
867 .with_interface(sample_interface()?)
868 .with_security_scheme(
869 "publicRead",
870 SecurityScheme::ApiKey(APIKeySecurityScheme {
871 description: None,
872 in_: APIKeyLocation::Header,
873 name: Arc::from(""),
874 }),
875 )
876 .with_input_mode("text/plain")
877 .with_output_mode("text/plain")
878 .build()
879 .expect_err("an empty API key parameter name must be refused");
880 assert_eq!(
881 err,
882 AgentCardError::EmptyAPIKeyParameterName {
883 name: Arc::from("publicRead"),
884 }
885 );
886 Ok(())
887 }
888
889 #[test]
890 fn empty_http_auth_scheme_is_rejected() -> Result<(), TestError> {
891 use super::super::security_scheme::{HTTPAuthSecurityScheme, SecurityScheme};
892
893 let err = AgentCardBuilder::new()
894 .with_name("n")
895 .with_description("d")
896 .with_version("1.0.0")
897 .with_interface(sample_interface()?)
898 .with_security_scheme(
899 "bearerAuth",
900 SecurityScheme::Http(HTTPAuthSecurityScheme {
901 description: None,
902 scheme: Arc::from(""),
903 bearer_format: None,
904 }),
905 )
906 .with_input_mode("text/plain")
907 .with_output_mode("text/plain")
908 .build()
909 .expect_err("an empty HTTP auth scheme must be refused");
910 assert_eq!(
911 err,
912 AgentCardError::EmptyHTTPAuthScheme {
913 name: Arc::from("bearerAuth"),
914 }
915 );
916 Ok(())
917 }
918
919 #[test]
920 fn unset_security_schemes_carries_empty_map() -> Result<(), TestError> {
921 let card = sample_card()?;
928 assert!(card.security_schemes.is_empty());
929 let value = serde_json::to_value(&card)?;
930 let obj = value
931 .as_object()
932 .expect("the card must serialise to a JSON object");
933 assert!(!obj.contains_key("securitySchemes"));
934 Ok(())
935 }
936}