1use std::fmt;
2
3use base64::{engine::general_purpose::STANDARD, Engine as _};
4use serde::{Deserialize, Serialize};
5
6use crate::{ArtifactKind, ArtifactReference};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase", deny_unknown_fields)]
10pub struct PutArtifactRequest {
11 pub kind: ArtifactKind,
12 pub mime_type: String,
13 pub data_base64: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
15 pub duration_millis: Option<u64>,
16}
17
18impl PutArtifactRequest {
19 pub fn decode(&self) -> Result<Vec<u8>, PutArtifactValidationError> {
20 if self.mime_type.trim() != self.mime_type || self.mime_type.is_empty() {
21 return Err(invalid("artifact put MIME type is invalid"));
22 }
23 match self.kind {
24 ArtifactKind::Image if !self.mime_type.starts_with("image/") => {
25 return Err(invalid("artifact put image MIME type is invalid"));
26 }
27 ArtifactKind::Audio if !self.mime_type.starts_with("audio/") => {
28 return Err(invalid("artifact put audio MIME type is invalid"));
29 }
30 ArtifactKind::Video if !self.mime_type.starts_with("video/") => {
31 return Err(invalid("artifact put video MIME type is invalid"));
32 }
33 ArtifactKind::File if self.mime_type.starts_with("video/") => {
34 return Err(invalid("video artifacts are not supported"));
35 }
36 _ => {}
37 }
38 if !matches!(self.kind, ArtifactKind::Audio | ArtifactKind::Video)
39 && self.duration_millis.is_some()
40 {
41 return Err(invalid(
42 "artifact put duration is only valid for audio or video",
43 ));
44 }
45 if self.duration_millis == Some(0) {
46 return Err(invalid("artifact put duration is invalid"));
47 }
48 let bytes = STANDARD
49 .decode(&self.data_base64)
50 .map_err(|_| invalid("artifact put content is invalid base64"))?;
51 if bytes.is_empty() {
52 return Err(invalid("artifact put content is empty"));
53 }
54 Ok(bytes)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase", deny_unknown_fields)]
60pub struct PutArtifactResponse {
61 pub uri: String,
62 pub kind: ArtifactKind,
63 pub mime_type: String,
64 pub size_bytes: u64,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub width: Option<u32>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub height: Option<u32>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub duration_millis: Option<u64>,
71}
72
73impl PutArtifactResponse {
74 pub fn validate(&self) -> Result<ArtifactReference, PutArtifactValidationError> {
75 let reference = ArtifactReference::parse(&self.uri)
76 .map_err(|_| invalid("artifact put URI is invalid"))?;
77 let metadata = reference.metadata();
78 if self.kind != metadata.kind()
79 || self.mime_type != metadata.mime_type()
80 || self.size_bytes != metadata.size_bytes()
81 || self.width != metadata.width()
82 || self.height != metadata.height()
83 || self.duration_millis != metadata.duration_millis()
84 {
85 return Err(invalid(
86 "artifact put metadata does not match its canonical reference",
87 ));
88 }
89 Ok(reference)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct PutArtifactValidationError(&'static str);
95
96impl fmt::Display for PutArtifactValidationError {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str(self.0)
99 }
100}
101
102impl std::error::Error for PutArtifactValidationError {}
103
104fn invalid(message: &'static str) -> PutArtifactValidationError {
105 PutArtifactValidationError(message)
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn request_rejects_kind_mime_mismatch() {
114 let request = PutArtifactRequest {
115 kind: ArtifactKind::Audio,
116 mime_type: "image/png".to_string(),
117 data_base64: "eA==".to_string(),
118 duration_millis: None,
119 };
120 assert!(request.decode().is_err());
121 }
122}