Skip to main content

zeph_a2a/
card.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::types::{AgentCapabilities, AgentCard, AgentProvider, AgentSkill};
5
6pub struct AgentCardBuilder {
7    name: String,
8    description: String,
9    url: String,
10    version: String,
11    capabilities: AgentCapabilities,
12    skills: Vec<AgentSkill>,
13    provider: Option<AgentProvider>,
14    input_modes: Vec<String>,
15    output_modes: Vec<String>,
16}
17
18impl AgentCardBuilder {
19    #[must_use]
20    pub fn new(
21        name: impl Into<String>,
22        url: impl Into<String>,
23        version: impl Into<String>,
24    ) -> Self {
25        Self {
26            name: name.into(),
27            description: String::new(),
28            url: url.into(),
29            version: version.into(),
30            capabilities: AgentCapabilities::default(),
31            skills: Vec::new(),
32            provider: None,
33            input_modes: Vec::new(),
34            output_modes: Vec::new(),
35        }
36    }
37
38    #[must_use]
39    pub fn description(mut self, desc: impl Into<String>) -> Self {
40        self.description = desc.into();
41        self
42    }
43
44    #[must_use]
45    pub fn streaming(mut self, enabled: bool) -> Self {
46        self.capabilities.streaming = enabled;
47        self
48    }
49
50    #[must_use]
51    pub fn push_notifications(mut self, enabled: bool) -> Self {
52        self.capabilities.push_notifications = enabled;
53        self
54    }
55
56    #[must_use]
57    pub fn skill(mut self, skill: AgentSkill) -> Self {
58        self.skills.push(skill);
59        self
60    }
61
62    #[must_use]
63    pub fn provider(mut self, org: impl Into<String>, url: impl Into<Option<String>>) -> Self {
64        self.provider = Some(AgentProvider {
65            organization: org.into(),
66            url: url.into(),
67        });
68        self
69    }
70
71    #[must_use]
72    pub fn default_input_modes(mut self, modes: Vec<String>) -> Self {
73        self.input_modes = modes;
74        self
75    }
76
77    #[must_use]
78    pub fn default_output_modes(mut self, modes: Vec<String>) -> Self {
79        self.output_modes = modes;
80        self
81    }
82
83    #[must_use]
84    pub fn build(self) -> AgentCard {
85        AgentCard {
86            name: self.name,
87            description: self.description,
88            url: self.url,
89            version: self.version,
90            provider: self.provider,
91            capabilities: self.capabilities,
92            default_input_modes: self.input_modes,
93            default_output_modes: self.output_modes,
94            skills: self.skills,
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn builder_minimal() {
105        let card = AgentCardBuilder::new("agent", "http://localhost", "0.1.0").build();
106        assert_eq!(card.name, "agent");
107        assert_eq!(card.url, "http://localhost");
108        assert_eq!(card.version, "0.1.0");
109        assert!(card.description.is_empty());
110        assert!(!card.capabilities.streaming);
111        assert!(card.skills.is_empty());
112    }
113
114    #[test]
115    fn builder_full() {
116        let card = AgentCardBuilder::new("zeph", "http://localhost:8080", "0.5.0")
117            .description("AI agent")
118            .streaming(true)
119            .push_notifications(false)
120            .provider("TestOrg", Some("https://test.org".into()))
121            .default_input_modes(vec!["text".into()])
122            .default_output_modes(vec!["text".into()])
123            .skill(AgentSkill {
124                id: "s1".into(),
125                name: "Skill One".into(),
126                description: "Does things".into(),
127                tags: vec!["test".into()],
128                examples: vec![],
129                input_modes: vec![],
130                output_modes: vec![],
131            })
132            .build();
133
134        assert_eq!(card.description, "AI agent");
135        assert!(card.capabilities.streaming);
136        assert!(!card.capabilities.push_notifications);
137        assert_eq!(card.provider.as_ref().unwrap().organization, "TestOrg");
138        assert_eq!(
139            card.provider.as_ref().unwrap().url.as_deref(),
140            Some("https://test.org")
141        );
142        assert_eq!(card.default_input_modes, vec!["text"]);
143        assert_eq!(card.skills.len(), 1);
144        assert_eq!(card.skills[0].id, "s1");
145    }
146
147    #[test]
148    fn builder_card_serializes() {
149        let card = AgentCardBuilder::new("test", "http://example.com", "1.0.0")
150            .description("test agent")
151            .build();
152        let json = serde_json::to_string(&card).unwrap();
153        assert!(json.contains("\"name\":\"test\""));
154        assert!(json.contains("\"defaultInputModes\"").not());
155    }
156
157    trait Not {
158        fn not(&self) -> bool;
159    }
160    impl Not for bool {
161        fn not(&self) -> bool {
162            !*self
163        }
164    }
165}