Skip to main content

artifact_api/
put.rs

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}