1use std::fmt;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct MediaAssetId(pub String);
11
12impl fmt::Display for MediaAssetId {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 write!(f, "{}", self.0)
15 }
16}
17
18impl<T: Into<String>> From<T> for MediaAssetId {
19 fn from(s: T) -> Self {
20 Self(s.into())
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct MediaAsset {
27 pub id: MediaAssetId,
29 pub provider: String,
31 pub media_type: MediaType,
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub url: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub filename: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub size_bytes: Option<u64>,
42 pub created_at: DateTime<Utc>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub raw: Option<serde_json::Value>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "snake_case")]
52pub enum MediaType {
53 Image,
55 Video,
57 Other(String),
59}
60
61impl fmt::Display for MediaType {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Image => write!(f, "image"),
65 Self::Video => write!(f, "video"),
66 Self::Other(s) => write!(f, "{s}"),
67 }
68 }
69}
70
71impl<'de> Deserialize<'de> for MediaType {
72 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
73 let s = String::deserialize(deserializer)?;
74 Ok(match s.as_str() {
75 "image" => Self::Image,
76 "video" => Self::Video,
77 _ => Self::Other(s),
78 })
79 }
80}
81
82#[derive(Debug, Clone, Default, Serialize, Deserialize)]
84pub struct UploadImageInput {
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub file_path: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub url: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub name: Option<String>,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
98pub struct UploadVideoInput {
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub file_path: Option<String>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub url: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub name: Option<String>,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub title: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub description: Option<String>,
114}
115
116impl crate::output::Formattable for MediaAsset {
117 fn headers() -> Vec<String> {
118 vec!["ID".into(), "Type".into(), "URL".into(), "Provider".into()]
119 }
120
121 fn row(&self) -> Vec<String> {
122 vec![
123 self.id.to_string(),
124 self.media_type.to_string(),
125 self.url.clone().unwrap_or_default(),
126 self.provider.clone(),
127 ]
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use test_case::test_case;
135
136 #[test]
137 fn media_asset_id_display() {
138 let id = MediaAssetId("media_123".into());
139 assert_eq!(id.to_string(), "media_123");
140 }
141
142 #[test]
143 fn media_asset_id_from_str() {
144 let id = MediaAssetId::from("media_456");
145 assert_eq!(id.0, "media_456");
146 }
147
148 #[test]
149 #[allow(clippy::expect_used)]
150 fn media_asset_id_serde_roundtrip() {
151 let id = MediaAssetId("media_789".into());
152 let json = serde_json::to_string(&id).expect("serialize MediaAssetId");
153 let back: MediaAssetId = serde_json::from_str(&json).expect("deserialize MediaAssetId");
154 assert_eq!(id, back);
155 }
156
157 #[test_case(MediaType::Image, "image" ; "image")]
158 #[test_case(MediaType::Video, "video" ; "video")]
159 #[allow(clippy::needless_pass_by_value)]
160 fn media_type_display(media_type: MediaType, expected: &str) {
161 assert_eq!(media_type.to_string(), expected);
162 }
163
164 #[test]
165 fn media_type_other_display() {
166 let t = MediaType::Other("audio".into());
167 assert_eq!(t.to_string(), "audio");
168 }
169
170 #[test]
171 #[allow(clippy::expect_used)]
172 fn media_type_serde_roundtrip() {
173 let json = serde_json::to_string(&MediaType::Image).expect("serialize");
174 assert_eq!(json, r#""image""#);
175 let back: MediaType = serde_json::from_str(&json).expect("deserialize");
176 assert_eq!(back, MediaType::Image);
177 }
178
179 #[test]
180 #[allow(clippy::expect_used)]
181 fn unknown_media_type_deserializes_as_other() {
182 let t: MediaType = serde_json::from_str(r#""document""#).expect("deserialize");
183 assert_eq!(t, MediaType::Other("document".into()));
184 }
185
186 #[test]
187 fn upload_image_input_default_all_none() {
188 let input = UploadImageInput::default();
189 assert!(input.file_path.is_none());
190 assert!(input.url.is_none());
191 assert!(input.name.is_none());
192 }
193
194 #[test]
195 fn upload_video_input_default_all_none() {
196 let input = UploadVideoInput::default();
197 assert!(input.file_path.is_none());
198 assert!(input.url.is_none());
199 assert!(input.name.is_none());
200 assert!(input.title.is_none());
201 assert!(input.description.is_none());
202 }
203
204 #[test]
205 #[allow(clippy::expect_used)]
206 fn upload_image_input_skips_none_fields() {
207 let input = UploadImageInput {
208 file_path: Some("/tmp/image.png".into()),
209 url: None,
210 name: None,
211 };
212 let json = serde_json::to_string(&input).expect("serialize UploadImageInput");
213 assert!(!json.contains("\"url\""));
214 assert!(!json.contains("\"name\""));
215 }
216
217 #[test]
218 #[allow(clippy::expect_used)]
219 fn upload_video_input_serde_roundtrip() {
220 let input = UploadVideoInput {
221 file_path: None,
222 url: Some("https://cdn.example.com/video.mp4".into()),
223 name: Some("my_video".into()),
224 title: Some("My Video".into()),
225 description: Some("A great video".into()),
226 };
227 let json = serde_json::to_string(&input).expect("serialize UploadVideoInput");
228 let back: UploadVideoInput =
229 serde_json::from_str(&json).expect("deserialize UploadVideoInput");
230 assert!(back.file_path.is_none());
231 assert_eq!(
232 back.url.as_deref(),
233 Some("https://cdn.example.com/video.mp4")
234 );
235 assert_eq!(back.title.as_deref(), Some("My Video"));
236 assert_eq!(back.description.as_deref(), Some("A great video"));
237 }
238
239 #[test]
240 #[allow(clippy::expect_used)]
241 fn media_asset_optional_fields_skip_serializing_if_none() {
242 let now = chrono::Utc::now();
243 let asset = MediaAsset {
244 id: MediaAssetId("m_1".into()),
245 provider: "meta".into(),
246 media_type: MediaType::Image,
247 url: None,
248 filename: None,
249 size_bytes: None,
250 created_at: now,
251 raw: None,
252 };
253 let json = serde_json::to_string(&asset).expect("serialize MediaAsset");
254 assert!(!json.contains("url"));
255 assert!(!json.contains("filename"));
256 assert!(!json.contains("size_bytes"));
257 assert!(!json.contains("raw"));
258 }
259}