use serde::{Deserialize, Serialize};
use crate::message::Part;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ArtifactId(pub String);
impl ArtifactId {
#[must_use]
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
}
impl std::fmt::Display for ArtifactId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for ArtifactId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ArtifactId {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
impl AsRef<str> for ArtifactId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Artifact {
#[serde(rename = "artifactId")]
pub id: ArtifactId,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Artifact {
#[must_use]
pub fn new(id: impl Into<ArtifactId>, parts: Vec<Part>) -> Self {
debug_assert!(
!parts.is_empty(),
"Artifact parts must not be empty per A2A spec"
);
Self {
id: id.into(),
name: None,
description: None,
parts,
extensions: None,
metadata: None,
}
}
pub fn validate(&self) -> Result<(), crate::error::A2aError> {
if self.parts.is_empty() {
return Err(crate::error::A2aError::invalid_params(
"artifact must contain at least one part",
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::Part;
#[test]
fn artifact_roundtrip() {
let artifact = Artifact::new("art-1", vec![Part::text("result content")]);
let json = serde_json::to_string(&artifact).expect("serialize");
assert!(json.contains("\"artifactId\":\"art-1\""));
assert!(json.contains("\"text\":\"result content\""));
let back: Artifact = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.id, ArtifactId::new("art-1"));
assert_eq!(back.parts.len(), 1);
}
#[test]
fn optional_fields_omitted() {
let artifact = Artifact::new("art-2", vec![Part::text("x")]);
let json = serde_json::to_string(&artifact).expect("serialize");
assert!(!json.contains("\"name\""), "name should be omitted");
assert!(
!json.contains("\"description\""),
"description should be omitted"
);
assert!(!json.contains("\"metadata\""), "metadata should be omitted");
}
#[test]
fn artifact_id_from_string() {
let id: ArtifactId = String::from("art-from-string").into();
assert_eq!(id, ArtifactId::new("art-from-string"));
}
#[test]
fn artifact_id_from_str() {
let id: ArtifactId = "art-from-str".into();
assert_eq!(id, ArtifactId::new("art-from-str"));
}
#[test]
fn artifact_id_as_ref() {
let id = ArtifactId::new("ref-test");
assert_eq!(id.as_ref(), "ref-test");
}
#[test]
fn artifact_new_optional_fields_are_none() {
let a = Artifact::new("id", vec![Part::text("x")]);
assert!(a.name.is_none());
assert!(a.description.is_none());
assert!(a.extensions.is_none());
assert!(a.metadata.is_none());
}
#[test]
fn artifact_id_display() {
let id = ArtifactId::new("my-artifact");
assert_eq!(id.to_string(), "my-artifact");
}
#[test]
fn validate_non_empty_parts_succeeds() {
let a = Artifact::new("art-ok", vec![Part::text("content")]);
assert!(a.validate().is_ok(), "artifact with parts should validate");
}
#[test]
fn validate_empty_parts_fails() {
let a = Artifact {
id: ArtifactId::new("art-empty"),
name: None,
description: None,
parts: vec![],
extensions: None,
metadata: None,
};
let err = a.validate().unwrap_err();
assert_eq!(
err.code,
crate::error::ErrorCode::InvalidParams,
"empty parts should return InvalidParams error"
);
assert!(
err.message.contains("at least one part"),
"error message should mention 'at least one part': {}",
err.message
);
}
}