use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use smbcloud_ascapi_core::jsonapi::{
CreateBody, CreateData, Document, OptionalDocument, Resource, ResourceId, ToOne, UpdateBody,
UpdateData, UpdateRelationshipsBody, UpdateRelationshipsData,
};
use smbcloud_ascapi_core::Client;
use smbcloud_ascapi_core::Result;
use crate::build::{Build, BuildAttributes};
pub const RESOURCE_TYPE: &str = "appStoreVersions";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Platform {
#[serde(rename = "IOS")]
Ios,
#[serde(rename = "MAC_OS")]
MacOs,
#[serde(rename = "TV_OS")]
TvOs,
#[serde(rename = "VISION_OS")]
VisionOs,
}
impl Platform {
pub(crate) fn as_query_value(self) -> &'static str {
match self {
Platform::Ios => "IOS",
Platform::MacOs => "MAC_OS",
Platform::TvOs => "TV_OS",
Platform::VisionOs => "VISION_OS",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionAttributes {
pub platform: Platform,
pub version_string: Option<String>,
pub app_version_state: Option<String>,
pub copyright: Option<String>,
pub created_date: Option<String>,
pub downloadable: Option<bool>,
}
pub type AppStoreVersion = Resource<AppStoreVersionAttributes>;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionCreateAttributes {
pub platform: Platform,
pub version_string: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub copyright: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionRelationships {
pub app: ToOne,
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionUpdateFields {
#[serde(skip_serializing_if = "Option::is_none")]
pub version_string: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub copyright: Option<String>,
}
#[async_trait]
pub trait AppStoreVersionsApi {
async fn list_app_store_versions(
&self,
app_id: &str,
filter_platform: Option<Platform>,
) -> Result<Vec<AppStoreVersion>>;
async fn get_app_store_version(&self, id: &str) -> Result<AppStoreVersion>;
async fn create_app_store_version(
&self,
app_id: &str,
attributes: AppStoreVersionCreateAttributes,
) -> Result<AppStoreVersion>;
async fn update_app_store_version(
&self,
id: &str,
fields: AppStoreVersionUpdateFields,
) -> Result<AppStoreVersion>;
async fn delete_app_store_version(&self, id: &str) -> Result<()>;
async fn set_app_store_version_build(
&self,
id: &str,
build_id: &str,
) -> Result<AppStoreVersion>;
async fn get_app_store_version_build(&self, id: &str) -> Result<Option<Build>>;
}
#[async_trait]
impl AppStoreVersionsApi for Client {
async fn list_app_store_versions(
&self,
app_id: &str,
filter_platform: Option<Platform>,
) -> Result<Vec<AppStoreVersion>> {
let path = format!("/v1/apps/{app_id}/appStoreVersions");
let mut query = Vec::new();
if let Some(platform) = filter_platform {
query.push(("filter[platform]", platform.as_query_value()));
}
self.list_all::<AppStoreVersionAttributes>(&path, &query)
.await
}
async fn get_app_store_version(&self, id: &str) -> Result<AppStoreVersion> {
let path = format!("/v1/appStoreVersions/{id}");
let doc: Document<AppStoreVersionAttributes> =
self.request(Method::GET, &path, &[], None::<&()>).await?;
Ok(doc.data)
}
async fn create_app_store_version(
&self,
app_id: &str,
attributes: AppStoreVersionCreateAttributes,
) -> Result<AppStoreVersion> {
let body = CreateBody {
data: CreateData {
resource_type: RESOURCE_TYPE,
attributes,
relationships: Some(AppStoreVersionRelationships {
app: ToOne {
data: ResourceId {
resource_type: crate::app::RESOURCE_TYPE,
id: app_id.to_string(),
},
},
}),
},
};
let doc: Document<AppStoreVersionAttributes> = self
.request(Method::POST, "/v1/appStoreVersions", &[], Some(&body))
.await?;
Ok(doc.data)
}
async fn update_app_store_version(
&self,
id: &str,
fields: AppStoreVersionUpdateFields,
) -> Result<AppStoreVersion> {
let path = format!("/v1/appStoreVersions/{id}");
let body = UpdateBody {
data: UpdateData {
resource_type: RESOURCE_TYPE,
id: id.to_string(),
attributes: fields,
},
};
let doc: Document<AppStoreVersionAttributes> =
self.request(Method::PATCH, &path, &[], Some(&body)).await?;
Ok(doc.data)
}
async fn delete_app_store_version(&self, id: &str) -> Result<()> {
let path = format!("/v1/appStoreVersions/{id}");
self.request_no_content::<()>(Method::DELETE, &path, &[], None)
.await
}
async fn set_app_store_version_build(
&self,
id: &str,
build_id: &str,
) -> Result<AppStoreVersion> {
let path = format!("/v1/appStoreVersions/{id}");
let body = UpdateRelationshipsBody {
data: UpdateRelationshipsData {
resource_type: RESOURCE_TYPE,
id: id.to_string(),
relationships: AppStoreVersionBuildRelationship {
build: ToOne {
data: ResourceId {
resource_type: crate::build::RESOURCE_TYPE,
id: build_id.to_string(),
},
},
},
},
};
let doc: Document<AppStoreVersionAttributes> =
self.request(Method::PATCH, &path, &[], Some(&body)).await?;
Ok(doc.data)
}
async fn get_app_store_version_build(&self, id: &str) -> Result<Option<Build>> {
let path = format!("/v1/appStoreVersions/{id}/build");
let doc: OptionalDocument<BuildAttributes> =
self.request(Method::GET, &path, &[], None::<&()>).await?;
Ok(doc.data)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppStoreVersionBuildRelationship {
pub build: ToOne,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_version_response_omitting_every_optional_attribute_still_parses() {
let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
"data": {
"id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
"type": "appStoreVersions",
"attributes": { "platform": "IOS" }
}
}))
.unwrap();
assert_eq!(doc.data.attributes.platform, Platform::Ios);
assert!(doc.data.attributes.version_string.is_none());
assert!(doc.data.attributes.copyright.is_none());
}
#[test]
fn a_version_response_reads_back_the_renamed_string() {
let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
"data": {
"id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
"type": "appStoreVersions",
"attributes": {
"platform": "MAC_OS",
"versionString": "1.1.0",
"appVersionState": "PREPARE_FOR_SUBMISSION",
"copyright": "2026 SplitFire AB"
}
}
}))
.unwrap();
assert_eq!(doc.data.attributes.platform, Platform::MacOs);
assert_eq!(doc.data.attributes.version_string.as_deref(), Some("1.1.0"));
assert_eq!(
doc.data.attributes.app_version_state.as_deref(),
Some("PREPARE_FOR_SUBMISSION")
);
}
#[test]
fn a_null_build_body_parses_to_none() {
let doc: OptionalDocument<BuildAttributes> =
serde_json::from_value(json!({ "data": null })).unwrap();
assert!(doc.data.is_none());
}
#[test]
fn a_populated_build_body_parses_with_its_version() {
let doc: OptionalDocument<BuildAttributes> = serde_json::from_value(json!({
"data": {
"id": "build-1",
"type": "builds",
"attributes": {
"version": "42",
"processingState": "VALID",
"usesNonExemptEncryption": false
}
}
}))
.unwrap();
let build = doc.data.unwrap();
assert_eq!(build.id, "build-1");
assert_eq!(build.attributes.version.as_deref(), Some("42"));
assert_eq!(build.attributes.uses_non_exempt_encryption, Some(false));
}
}