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