1use 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#[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 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#[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}