a2a_protocol_types/
artifact.rs1use serde::{Deserialize, Serialize};
14
15use crate::message::Part;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct ArtifactId(pub String);
24
25impl ArtifactId {
26 #[must_use]
28 pub fn new(s: impl Into<String>) -> Self {
29 Self(s.into())
30 }
31}
32
33impl std::fmt::Display for ArtifactId {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.write_str(&self.0)
36 }
37}
38
39impl From<String> for ArtifactId {
40 fn from(s: String) -> Self {
41 Self(s)
42 }
43}
44
45impl From<&str> for ArtifactId {
46 fn from(s: &str) -> Self {
47 Self(s.to_owned())
48 }
49}
50
51impl AsRef<str> for ArtifactId {
52 fn as_ref(&self) -> &str {
53 &self.0
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct Artifact {
66 #[serde(rename = "artifactId")]
68 pub id: ArtifactId,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub name: Option<String>,
73
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub description: Option<String>,
77
78 pub parts: Vec<Part>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub extensions: Option<Vec<String>>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub metadata: Option<serde_json::Value>,
91}
92
93impl Artifact {
94 #[must_use]
101 pub fn new(id: impl Into<ArtifactId>, parts: Vec<Part>) -> Self {
102 debug_assert!(
103 !parts.is_empty(),
104 "Artifact parts must not be empty per A2A spec"
105 );
106 Self {
107 id: id.into(),
108 name: None,
109 description: None,
110 parts,
111 extensions: None,
112 metadata: None,
113 }
114 }
115
116 pub fn validate(&self) -> Result<(), crate::error::A2aError> {
124 if self.parts.is_empty() {
125 return Err(crate::error::A2aError::invalid_params(
126 "artifact must contain at least one part",
127 ));
128 }
129 Ok(())
130 }
131}
132
133#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::message::Part;
139
140 #[test]
141 fn artifact_roundtrip() {
142 let artifact = Artifact::new("art-1", vec![Part::text("result content")]);
143 let json = serde_json::to_string(&artifact).expect("serialize");
144 assert!(json.contains("\"artifactId\":\"art-1\""));
145 assert!(json.contains("\"text\":\"result content\""));
146
147 let back: Artifact = serde_json::from_str(&json).expect("deserialize");
148 assert_eq!(back.id, ArtifactId::new("art-1"));
149 assert_eq!(back.parts.len(), 1);
150 }
151
152 #[test]
153 fn optional_fields_omitted() {
154 let artifact = Artifact::new("art-2", vec![Part::text("x")]);
155 let json = serde_json::to_string(&artifact).expect("serialize");
156 assert!(!json.contains("\"name\""), "name should be omitted");
157 assert!(
158 !json.contains("\"description\""),
159 "description should be omitted"
160 );
161 assert!(!json.contains("\"metadata\""), "metadata should be omitted");
162 }
163
164 #[test]
165 fn artifact_id_from_string() {
166 let id: ArtifactId = String::from("art-from-string").into();
167 assert_eq!(id, ArtifactId::new("art-from-string"));
168 }
169
170 #[test]
171 fn artifact_id_from_str() {
172 let id: ArtifactId = "art-from-str".into();
173 assert_eq!(id, ArtifactId::new("art-from-str"));
174 }
175
176 #[test]
177 fn artifact_id_as_ref() {
178 let id = ArtifactId::new("ref-test");
179 assert_eq!(id.as_ref(), "ref-test");
180 }
181
182 #[test]
183 fn artifact_new_optional_fields_are_none() {
184 let a = Artifact::new("id", vec![Part::text("x")]);
185 assert!(a.name.is_none());
186 assert!(a.description.is_none());
187 assert!(a.extensions.is_none());
188 assert!(a.metadata.is_none());
189 }
190
191 #[test]
192 fn artifact_id_display() {
193 let id = ArtifactId::new("my-artifact");
194 assert_eq!(id.to_string(), "my-artifact");
195 }
196
197 #[test]
200 fn validate_non_empty_parts_succeeds() {
201 let a = Artifact::new("art-ok", vec![Part::text("content")]);
202 assert!(a.validate().is_ok(), "artifact with parts should validate");
203 }
204
205 #[test]
206 fn validate_empty_parts_fails() {
207 let a = Artifact {
208 id: ArtifactId::new("art-empty"),
209 name: None,
210 description: None,
211 parts: vec![],
212 extensions: None,
213 metadata: None,
214 };
215 let err = a.validate().unwrap_err();
216 assert_eq!(
217 err.code,
218 crate::error::ErrorCode::InvalidParams,
219 "empty parts should return InvalidParams error"
220 );
221 assert!(
222 err.message.contains("at least one part"),
223 "error message should mention 'at least one part': {}",
224 err.message
225 );
226 }
227}