1use async_trait::async_trait;
16use reqwest::Method;
17use serde::{Deserialize, Deserializer, Serialize};
18use smbcloud_ascapi_core::jsonapi::{
19 CreateBody, CreateData, Document, Resource, ResourceId, ToOne, UpdateBody, UpdateData,
20};
21use smbcloud_ascapi_core::Client;
22use smbcloud_ascapi_core::Result;
23
24pub const RESOURCE_TYPE: &str = "appScreenshots";
25
26fn null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
31where
32 D: Deserializer<'de>,
33 T: Default + Deserialize<'de>,
34{
35 Ok(Option::deserialize(deserializer)?.unwrap_or_default())
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct RequestHeader {
41 pub name: String,
42 pub value: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct UploadOperation {
48 pub method: String,
49 pub url: String,
50 #[serde(default)]
51 pub length: u64,
52 #[serde(default)]
53 pub offset: u64,
54 #[serde(default, deserialize_with = "null_as_default")]
55 pub request_headers: Vec<RequestHeader>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct AssetDeliveryState {
61 pub state: String,
62 #[serde(default, deserialize_with = "null_as_default")]
63 pub errors: Vec<serde_json::Value>,
64 #[serde(default, deserialize_with = "null_as_default")]
65 pub warnings: Vec<serde_json::Value>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct AppScreenshotAttributes {
71 pub file_name: String,
72 pub file_size: u64,
73 #[serde(default)]
74 pub source_file_checksum: Option<String>,
75 #[serde(default)]
76 pub image_asset: Option<serde_json::Value>,
77 #[serde(default)]
78 pub asset_delivery_state: Option<AssetDeliveryState>,
79 #[serde(default, deserialize_with = "null_as_default")]
80 pub upload_operations: Vec<UploadOperation>,
81 #[serde(default)]
82 pub uploaded: Option<bool>,
83}
84
85pub type AppScreenshot = Resource<AppScreenshotAttributes>;
86
87#[derive(Debug, Clone, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct AppScreenshotCreateAttributes {
90 pub file_name: String,
91 pub file_size: u64,
92}
93
94#[derive(Debug, Clone, Serialize)]
95#[serde(rename_all = "camelCase")]
96pub struct AppScreenshotSetRelationship {
97 pub app_screenshot_set: ToOne,
98}
99
100#[derive(Debug, Clone, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct AppScreenshotCommitAttributes {
103 pub uploaded: bool,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub source_file_checksum: Option<String>,
106}
107
108#[async_trait]
115pub trait AppScreenshotsApi {
116 async fn list_app_screenshots(&self, app_screenshot_set_id: &str)
118 -> Result<Vec<AppScreenshot>>;
119
120 async fn create_app_screenshot(
124 &self,
125 app_screenshot_set_id: &str,
126 attributes: AppScreenshotCreateAttributes,
127 ) -> Result<AppScreenshot>;
128
129 async fn upload_app_screenshot_bytes(
135 &self,
136 screenshot: &AppScreenshot,
137 bytes: &[u8],
138 ) -> Result<()>;
139
140 async fn commit_app_screenshot(
144 &self,
145 id: &str,
146 source_file_checksum: String,
147 ) -> Result<AppScreenshot>;
148
149 async fn delete_app_screenshot(&self, id: &str) -> Result<()>;
151
152 async fn upload_app_screenshot(
159 &self,
160 app_screenshot_set_id: &str,
161 file_name: String,
162 bytes: Vec<u8>,
163 ) -> Result<AppScreenshot>;
164}
165
166#[async_trait]
167impl AppScreenshotsApi for Client {
168 async fn list_app_screenshots(
169 &self,
170 app_screenshot_set_id: &str,
171 ) -> Result<Vec<AppScreenshot>> {
172 let path = format!("/v1/appScreenshotSets/{app_screenshot_set_id}/appScreenshots");
173 self.list_all::<AppScreenshotAttributes>(&path, &[]).await
174 }
175
176 async fn create_app_screenshot(
177 &self,
178 app_screenshot_set_id: &str,
179 attributes: AppScreenshotCreateAttributes,
180 ) -> Result<AppScreenshot> {
181 let body = CreateBody {
182 data: CreateData {
183 resource_type: RESOURCE_TYPE,
184 attributes,
185 relationships: Some(AppScreenshotSetRelationship {
186 app_screenshot_set: ToOne {
187 data: ResourceId {
188 resource_type: crate::app_screenshot_set::RESOURCE_TYPE,
189 id: app_screenshot_set_id.to_string(),
190 },
191 },
192 }),
193 },
194 };
195 let doc: Document<AppScreenshotAttributes> = self
196 .request(Method::POST, "/v1/appScreenshots", &[], Some(&body))
197 .await?;
198 Ok(doc.data)
199 }
200
201 async fn upload_app_screenshot_bytes(
202 &self,
203 screenshot: &AppScreenshot,
204 bytes: &[u8],
205 ) -> Result<()> {
206 for op in &screenshot.attributes.upload_operations {
207 let start = op.offset as usize;
208 let end = start + op.length as usize;
209 let chunk = bytes
210 .get(start..end)
211 .map(|slice| slice.to_vec())
212 .unwrap_or_default();
213 let headers: Vec<(String, String)> = op
214 .request_headers
215 .iter()
216 .map(|h| (h.name.clone(), h.value.clone()))
217 .collect();
218 let method: Method = op.method.parse().unwrap_or(Method::PUT);
219 self.upload_bytes(method, &op.url, &headers, chunk).await?;
220 }
221 Ok(())
222 }
223
224 async fn commit_app_screenshot(
225 &self,
226 id: &str,
227 source_file_checksum: String,
228 ) -> Result<AppScreenshot> {
229 let path = format!("/v1/appScreenshots/{id}");
230 let body = UpdateBody {
231 data: UpdateData {
232 resource_type: RESOURCE_TYPE,
233 id: id.to_string(),
234 attributes: AppScreenshotCommitAttributes {
235 uploaded: true,
236 source_file_checksum: Some(source_file_checksum),
237 },
238 },
239 };
240 let doc: Document<AppScreenshotAttributes> =
241 self.request(Method::PATCH, &path, &[], Some(&body)).await?;
242 Ok(doc.data)
243 }
244
245 async fn delete_app_screenshot(&self, id: &str) -> Result<()> {
246 let path = format!("/v1/appScreenshots/{id}");
247 self.request_no_content::<()>(Method::DELETE, &path, &[], None)
248 .await
249 }
250
251 async fn upload_app_screenshot(
252 &self,
253 app_screenshot_set_id: &str,
254 file_name: String,
255 bytes: Vec<u8>,
256 ) -> Result<AppScreenshot> {
257 let checksum = format!("{:x}", md5::compute(&bytes));
258 let screenshot = self
259 .create_app_screenshot(
260 app_screenshot_set_id,
261 AppScreenshotCreateAttributes {
262 file_name,
263 file_size: bytes.len() as u64,
264 },
265 )
266 .await?;
267 self.upload_app_screenshot_bytes(&screenshot, &bytes)
268 .await?;
269 self.commit_app_screenshot(&screenshot.id, checksum).await
270 }
271}