Skip to main content

artifact_api/
media.rs

1//! Media values refer only to committed, scope-owned artifacts.
2use crate::{ArtifactKind, ArtifactReference, ArtifactReferenceError};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(deny_unknown_fields)]
7pub struct MediaReference {
8    pub uri: ArtifactUri,
9}
10
11impl MediaReference {
12    pub fn reference(
13        &self,
14        kind: ArtifactKind,
15    ) -> Result<ArtifactReference, ArtifactReferenceError> {
16        let reference = ArtifactReference::parse(&self.uri)?;
17        if reference.metadata().kind() != kind {
18            return Err(super::invalid(
19                "media artifact kind does not match the field type",
20            ));
21        }
22        Ok(reference)
23    }
24
25    pub fn validate(
26        &self,
27        tenant: &str,
28        scope: &str,
29        kind: ArtifactKind,
30    ) -> Result<ArtifactReference, ArtifactReferenceError> {
31        let reference = self.reference(kind)?;
32        reference.ensure_scope(tenant, scope)?;
33        Ok(reference)
34    }
35}
36
37/// Explicit import of a finite HTTP resource. Scope comes from authenticated context.
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40pub struct ImportArtifactRequest {
41    #[serde(default)]
42    pub identity: crate::ArtifactIdentity,
43    #[serde(default)]
44    pub retention: crate::ArtifactRetention,
45    #[serde(
46        default,
47        skip_serializing_if = "Option::is_none",
48        deserialize_with = "crate::deserialize_optional_non_null"
49    )]
50    pub creation_id: Option<String>,
51    pub kind: ArtifactKind,
52    pub source_url: String,
53    #[serde(
54        default,
55        skip_serializing_if = "Option::is_none",
56        deserialize_with = "crate::deserialize_optional_non_null"
57    )]
58    pub mime_type: Option<String>,
59}
60
61impl ImportArtifactRequest {
62    /// Validate caller metadata before any source lookup. HTTP response MIME
63    /// headers are normalized separately by the importer when no override is given.
64    pub fn validate(&self) -> Result<(), ArtifactReferenceError> {
65        self.policy()
66            .validate_creation_id(self.creation_id.as_deref())?;
67        if self.kind == ArtifactKind::Video {
68            return Err(crate::invalid("video artifact imports are not supported"));
69        }
70        if let Some(mime) = &self.mime_type {
71            crate::validate_mime_type(mime)?;
72            match self.kind {
73                ArtifactKind::Image if !mime.starts_with("image/") => {
74                    return Err(crate::invalid("image artifact MIME type is invalid"));
75                }
76                ArtifactKind::Audio if !mime.starts_with("audio/") => {
77                    return Err(crate::invalid("audio artifact MIME type is invalid"));
78                }
79                ArtifactKind::File if mime.starts_with("video/") => {
80                    return Err(crate::invalid("video artifacts are not supported"));
81                }
82                _ => {}
83            }
84        }
85        Ok(())
86    }
87
88    pub const fn policy(&self) -> crate::ArtifactPolicy {
89        crate::ArtifactPolicy::new(self.identity, self.retention)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    #[test]
97    fn media_rejects_legacy_fields_and_wrong_scope_or_kind() {
98        for value in [
99            serde_json::json!({"url":"https://example.com/a"}),
100            serde_json::json!({"uri":"x","base64":"eA=="}),
101            serde_json::json!({"uri":"x","mimeType":"image/png"}),
102            serde_json::json!({"uri":"x","createdAt":1}),
103        ] {
104            assert!(serde_json::from_value::<MediaReference>(value).is_err());
105        }
106        let reference = ArtifactReference::new(
107            "tenant",
108            "scope",
109            "a".repeat(64),
110            crate::ArtifactMetadata::audio("audio/mpeg", 10, None).unwrap(),
111        )
112        .unwrap();
113        let media = MediaReference {
114            uri: reference.uri().unwrap().parse().unwrap(),
115        };
116        assert!(media
117            .validate("tenant", "scope", ArtifactKind::Audio)
118            .is_ok());
119        assert!(media
120            .validate("tenant", "other", ArtifactKind::Audio)
121            .is_err());
122        assert!(media
123            .validate("tenant", "scope", ArtifactKind::Image)
124            .is_err());
125        assert!(serde_json::from_value::<MediaReference>(serde_json::json!({
126            "uri": "https://example.com/a"
127        }))
128        .is_err());
129    }
130}
131
132/// A canonical artifact URI. Construction and deserialization reject non-artifact sources.
133#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(try_from = "String", into = "String")]
135pub struct ArtifactUri(String);
136
137impl ArtifactUri {
138    pub fn as_str(&self) -> &str {
139        &self.0
140    }
141    pub fn reference(&self) -> ArtifactReference {
142        ArtifactReference::parse(&self.0).expect("ArtifactUri was validated at construction")
143    }
144}
145impl std::str::FromStr for ArtifactUri {
146    type Err = ArtifactReferenceError;
147    fn from_str(value: &str) -> Result<Self, Self::Err> {
148        let reference = ArtifactReference::parse(value)?;
149        Ok(Self(reference.uri()?))
150    }
151}
152impl TryFrom<String> for ArtifactUri {
153    type Error = ArtifactReferenceError;
154    fn try_from(value: String) -> Result<Self, Self::Error> {
155        value.parse()
156    }
157}
158impl From<ArtifactUri> for String {
159    fn from(value: ArtifactUri) -> String {
160        value.0
161    }
162}
163impl std::ops::Deref for ArtifactUri {
164    type Target = str;
165    fn deref(&self) -> &str {
166        self.as_str()
167    }
168}
169impl AsRef<str> for ArtifactUri {
170    fn as_ref(&self) -> &str {
171        self.as_str()
172    }
173}
174impl std::fmt::Display for ArtifactUri {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        self.0.fmt(f)
177    }
178}