1use async_trait::async_trait;
9use reqwest::Method;
10use serde::{Deserialize, Serialize};
11use smbcloud_ascapi_core::jsonapi::{
12 CreateBody, CreateData, Document, OptionalDocument, Resource, ResourceId, ToOne, UpdateBody,
13 UpdateData, UpdateRelationshipsBody, UpdateRelationshipsData,
14};
15use smbcloud_ascapi_core::Client;
16use smbcloud_ascapi_core::Result;
17
18use crate::build::{Build, BuildAttributes};
19
20pub const RESOURCE_TYPE: &str = "appStoreVersions";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub enum Platform {
24 #[serde(rename = "IOS")]
25 Ios,
26 #[serde(rename = "MAC_OS")]
27 MacOs,
28 #[serde(rename = "TV_OS")]
29 TvOs,
30 #[serde(rename = "VISION_OS")]
31 VisionOs,
32}
33
34impl Platform {
35 pub(crate) fn as_query_value(self) -> &'static str {
36 match self {
37 Platform::Ios => "IOS",
38 Platform::MacOs => "MAC_OS",
39 Platform::TvOs => "TV_OS",
40 Platform::VisionOs => "VISION_OS",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct AppStoreVersionAttributes {
48 pub platform: Platform,
49 pub version_string: Option<String>,
50 pub app_version_state: Option<String>,
51 pub copyright: Option<String>,
52 pub created_date: Option<String>,
53 pub downloadable: Option<bool>,
54}
55
56pub type AppStoreVersion = Resource<AppStoreVersionAttributes>;
57
58#[derive(Debug, Clone, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct AppStoreVersionCreateAttributes {
61 pub platform: Platform,
62 pub version_string: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub copyright: Option<String>,
65}
66
67#[derive(Debug, Clone, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct AppStoreVersionRelationships {
70 pub app: ToOne,
71}
72
73#[derive(Debug, Clone, Default, Serialize)]
80#[serde(rename_all = "camelCase")]
81pub struct AppStoreVersionUpdateFields {
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub version_string: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub copyright: Option<String>,
86}
87
88#[async_trait]
95pub trait AppStoreVersionsApi {
96 async fn list_app_store_versions(
99 &self,
100 app_id: &str,
101 filter_platform: Option<Platform>,
102 ) -> Result<Vec<AppStoreVersion>>;
103
104 async fn get_app_store_version(&self, id: &str) -> Result<AppStoreVersion>;
105
106 async fn create_app_store_version(
108 &self,
109 app_id: &str,
110 attributes: AppStoreVersionCreateAttributes,
111 ) -> Result<AppStoreVersion>;
112
113 async fn update_app_store_version(
117 &self,
118 id: &str,
119 fields: AppStoreVersionUpdateFields,
120 ) -> Result<AppStoreVersion>;
121
122 async fn delete_app_store_version(&self, id: &str) -> Result<()>;
125
126 async fn set_app_store_version_build(
133 &self,
134 id: &str,
135 build_id: &str,
136 ) -> Result<AppStoreVersion>;
137
138 async fn get_app_store_version_build(&self, id: &str) -> Result<Option<Build>>;
143}
144
145#[async_trait]
146impl AppStoreVersionsApi for Client {
147 async fn list_app_store_versions(
148 &self,
149 app_id: &str,
150 filter_platform: Option<Platform>,
151 ) -> Result<Vec<AppStoreVersion>> {
152 let path = format!("/v1/apps/{app_id}/appStoreVersions");
153 let mut query = Vec::new();
154 if let Some(platform) = filter_platform {
155 query.push(("filter[platform]", platform.as_query_value()));
156 }
157 self.list_all::<AppStoreVersionAttributes>(&path, &query)
158 .await
159 }
160
161 async fn get_app_store_version(&self, id: &str) -> Result<AppStoreVersion> {
162 let path = format!("/v1/appStoreVersions/{id}");
163 let doc: Document<AppStoreVersionAttributes> =
164 self.request(Method::GET, &path, &[], None::<&()>).await?;
165 Ok(doc.data)
166 }
167
168 async fn create_app_store_version(
169 &self,
170 app_id: &str,
171 attributes: AppStoreVersionCreateAttributes,
172 ) -> Result<AppStoreVersion> {
173 let body = CreateBody {
174 data: CreateData {
175 resource_type: RESOURCE_TYPE,
176 attributes,
177 relationships: Some(AppStoreVersionRelationships {
178 app: ToOne {
179 data: ResourceId {
180 resource_type: crate::app::RESOURCE_TYPE,
181 id: app_id.to_string(),
182 },
183 },
184 }),
185 },
186 };
187 let doc: Document<AppStoreVersionAttributes> = self
188 .request(Method::POST, "/v1/appStoreVersions", &[], Some(&body))
189 .await?;
190 Ok(doc.data)
191 }
192
193 async fn update_app_store_version(
194 &self,
195 id: &str,
196 fields: AppStoreVersionUpdateFields,
197 ) -> Result<AppStoreVersion> {
198 let path = format!("/v1/appStoreVersions/{id}");
199 let body = UpdateBody {
200 data: UpdateData {
201 resource_type: RESOURCE_TYPE,
202 id: id.to_string(),
203 attributes: fields,
204 },
205 };
206 let doc: Document<AppStoreVersionAttributes> =
207 self.request(Method::PATCH, &path, &[], Some(&body)).await?;
208 Ok(doc.data)
209 }
210
211 async fn delete_app_store_version(&self, id: &str) -> Result<()> {
212 let path = format!("/v1/appStoreVersions/{id}");
213 self.request_no_content::<()>(Method::DELETE, &path, &[], None)
214 .await
215 }
216
217 async fn set_app_store_version_build(
218 &self,
219 id: &str,
220 build_id: &str,
221 ) -> Result<AppStoreVersion> {
222 let path = format!("/v1/appStoreVersions/{id}");
223 let body = UpdateRelationshipsBody {
224 data: UpdateRelationshipsData {
225 resource_type: RESOURCE_TYPE,
226 id: id.to_string(),
227 relationships: AppStoreVersionBuildRelationship {
228 build: ToOne {
229 data: ResourceId {
230 resource_type: crate::build::RESOURCE_TYPE,
231 id: build_id.to_string(),
232 },
233 },
234 },
235 },
236 };
237 let doc: Document<AppStoreVersionAttributes> =
238 self.request(Method::PATCH, &path, &[], Some(&body)).await?;
239 Ok(doc.data)
240 }
241
242 async fn get_app_store_version_build(&self, id: &str) -> Result<Option<Build>> {
243 let path = format!("/v1/appStoreVersions/{id}/build");
244 let doc: OptionalDocument<BuildAttributes> =
245 self.request(Method::GET, &path, &[], None::<&()>).await?;
246 Ok(doc.data)
247 }
248}
249
250#[derive(Debug, Clone, Serialize)]
254#[serde(rename_all = "camelCase")]
255pub struct AppStoreVersionBuildRelationship {
256 pub build: ToOne,
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use serde_json::json;
263
264 #[test]
271 fn a_version_response_omitting_every_optional_attribute_still_parses() {
272 let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
273 "data": {
274 "id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
275 "type": "appStoreVersions",
276 "attributes": { "platform": "IOS" }
277 }
278 }))
279 .unwrap();
280
281 assert_eq!(doc.data.attributes.platform, Platform::Ios);
282 assert!(doc.data.attributes.version_string.is_none());
283 assert!(doc.data.attributes.copyright.is_none());
284 }
285
286 #[test]
290 fn a_version_response_reads_back_the_renamed_string() {
291 let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
292 "data": {
293 "id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
294 "type": "appStoreVersions",
295 "attributes": {
296 "platform": "MAC_OS",
297 "versionString": "1.1.0",
298 "appVersionState": "PREPARE_FOR_SUBMISSION",
299 "copyright": "2026 SplitFire AB"
300 }
301 }
302 }))
303 .unwrap();
304
305 assert_eq!(doc.data.attributes.platform, Platform::MacOs);
306 assert_eq!(doc.data.attributes.version_string.as_deref(), Some("1.1.0"));
307 assert_eq!(
308 doc.data.attributes.app_version_state.as_deref(),
309 Some("PREPARE_FOR_SUBMISSION")
310 );
311 }
312
313 #[test]
316 fn a_null_build_body_parses_to_none() {
317 let doc: OptionalDocument<BuildAttributes> =
318 serde_json::from_value(json!({ "data": null })).unwrap();
319 assert!(doc.data.is_none());
320 }
321
322 #[test]
323 fn a_populated_build_body_parses_with_its_version() {
324 let doc: OptionalDocument<BuildAttributes> = serde_json::from_value(json!({
325 "data": {
326 "id": "build-1",
327 "type": "builds",
328 "attributes": {
329 "version": "42",
330 "processingState": "VALID",
331 "usesNonExemptEncryption": false
332 }
333 }
334 }))
335 .unwrap();
336
337 let build = doc.data.unwrap();
338 assert_eq!(build.id, "build-1");
339 assert_eq!(build.attributes.version.as_deref(), Some("42"));
340 assert_eq!(build.attributes.uses_non_exempt_encryption, Some(false));
341 }
342}