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 lifecycle: crate::ArtifactLifecycle,
43    #[serde(
44        default,
45        skip_serializing_if = "Option::is_none",
46        deserialize_with = "crate::deserialize_optional_non_null"
47    )]
48    pub creation_id: Option<String>,
49    pub kind: ArtifactKind,
50    pub source_url: String,
51    #[serde(
52        default,
53        skip_serializing_if = "Option::is_none",
54        deserialize_with = "crate::deserialize_optional_non_null"
55    )]
56    pub mime_type: Option<String>,
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    #[test]
63    fn media_rejects_legacy_fields_and_wrong_scope_or_kind() {
64        for value in [
65            serde_json::json!({"url":"https://example.com/a"}),
66            serde_json::json!({"uri":"x","base64":"eA=="}),
67            serde_json::json!({"uri":"x","mimeType":"image/png"}),
68            serde_json::json!({"uri":"x","createdAt":1}),
69        ] {
70            assert!(serde_json::from_value::<MediaReference>(value).is_err());
71        }
72        let reference = ArtifactReference::new(
73            "tenant",
74            "scope",
75            "a".repeat(64),
76            crate::ArtifactMetadata::audio("audio/mpeg", 10, None).unwrap(),
77        )
78        .unwrap();
79        let media = MediaReference {
80            uri: reference.uri().unwrap().parse().unwrap(),
81        };
82        assert!(media
83            .validate("tenant", "scope", ArtifactKind::Audio)
84            .is_ok());
85        assert!(media
86            .validate("tenant", "other", ArtifactKind::Audio)
87            .is_err());
88        assert!(media
89            .validate("tenant", "scope", ArtifactKind::Image)
90            .is_err());
91        assert!(serde_json::from_value::<MediaReference>(serde_json::json!({
92            "uri": "https://example.com/a"
93        }))
94        .is_err());
95    }
96}
97
98/// A canonical artifact URI. Construction and deserialization reject non-artifact sources.
99#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
100#[serde(try_from = "String", into = "String")]
101pub struct ArtifactUri(String);
102
103impl ArtifactUri {
104    pub fn as_str(&self) -> &str {
105        &self.0
106    }
107    pub fn reference(&self) -> ArtifactReference {
108        ArtifactReference::parse(&self.0).expect("ArtifactUri was validated at construction")
109    }
110}
111impl std::str::FromStr for ArtifactUri {
112    type Err = ArtifactReferenceError;
113    fn from_str(value: &str) -> Result<Self, Self::Err> {
114        let reference = ArtifactReference::parse(value)?;
115        Ok(Self(reference.uri()?))
116    }
117}
118impl TryFrom<String> for ArtifactUri {
119    type Error = ArtifactReferenceError;
120    fn try_from(value: String) -> Result<Self, Self::Error> {
121        value.parse()
122    }
123}
124impl From<ArtifactUri> for String {
125    fn from(value: ArtifactUri) -> String {
126        value.0
127    }
128}
129impl std::ops::Deref for ArtifactUri {
130    type Target = str;
131    fn deref(&self) -> &str {
132        self.as_str()
133    }
134}
135impl AsRef<str> for ArtifactUri {
136    fn as_ref(&self) -> &str {
137        self.as_str()
138    }
139}
140impl std::fmt::Display for ArtifactUri {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        self.0.fmt(f)
143    }
144}