use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use smbcloud_ascapi_core::jsonapi::{Document, Resource, UpdateBody, UpdateData};
use smbcloud_ascapi_core::Client;
use smbcloud_ascapi_core::Result;
use crate::app_store_version::Platform;
pub const RESOURCE_TYPE: &str = "builds";
pub const PRE_RELEASE_VERSION_RESOURCE_TYPE: &str = "preReleaseVersions";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildAttributes {
pub version: Option<String>,
pub uploaded_date: Option<String>,
pub expired: Option<bool>,
pub processing_state: Option<String>,
pub min_os_version: Option<String>,
pub uses_non_exempt_encryption: Option<bool>,
}
pub type Build = Resource<BuildAttributes>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreReleaseVersionAttributes {
pub version: Option<String>,
pub platform: Option<Platform>,
}
pub type PreReleaseVersion = Resource<PreReleaseVersionAttributes>;
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildUpdateAttributes {
#[serde(skip_serializing_if = "Option::is_none")]
pub uses_non_exempt_encryption: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct BuildFilter {
pub version: Option<String>,
pub pre_release_version: Option<String>,
pub processing_state: Option<String>,
}
impl BuildFilter {
fn query_pairs(&self) -> Vec<(&'static str, String)> {
let mut pairs = Vec::new();
if let Some(v) = &self.version {
pairs.push(("filter[version]", v.clone()));
}
if let Some(v) = &self.pre_release_version {
pairs.push(("filter[preReleaseVersion.version]", v.clone()));
}
if let Some(v) = &self.processing_state {
pairs.push(("filter[processingState]", v.clone()));
}
pairs
}
}
#[async_trait]
pub trait BuildsApi {
async fn list_builds(&self, app_id: &str, filter: &BuildFilter) -> Result<Vec<Build>>;
async fn get_build(&self, id: &str) -> Result<Build>;
async fn update_build(&self, id: &str, attributes: BuildUpdateAttributes) -> Result<Build>;
async fn list_pre_release_versions(
&self,
app_id: &str,
filter_platform: Option<Platform>,
) -> Result<Vec<PreReleaseVersion>>;
}
#[async_trait]
impl BuildsApi for Client {
async fn list_builds(&self, app_id: &str, filter: &BuildFilter) -> Result<Vec<Build>> {
let owned = filter.query_pairs();
let mut query: Vec<(&str, &str)> = vec![("filter[app]", app_id), ("limit", "200")];
query.extend(owned.iter().map(|(k, v)| (*k, v.as_str())));
let mut builds = self
.list_all::<BuildAttributes>("/v1/builds", &query)
.await?;
builds.sort_by(|a, b| b.attributes.uploaded_date.cmp(&a.attributes.uploaded_date));
Ok(builds)
}
async fn get_build(&self, id: &str) -> Result<Build> {
let path = format!("/v1/builds/{id}");
let doc: Document<BuildAttributes> =
self.request(Method::GET, &path, &[], None::<&()>).await?;
Ok(doc.data)
}
async fn update_build(&self, id: &str, attributes: BuildUpdateAttributes) -> Result<Build> {
let path = format!("/v1/builds/{id}");
let body = UpdateBody {
data: UpdateData {
resource_type: RESOURCE_TYPE,
id: id.to_string(),
attributes,
},
};
let doc: Document<BuildAttributes> =
self.request(Method::PATCH, &path, &[], Some(&body)).await?;
Ok(doc.data)
}
async fn list_pre_release_versions(
&self,
app_id: &str,
filter_platform: Option<Platform>,
) -> Result<Vec<PreReleaseVersion>> {
let mut query: Vec<(&str, &str)> = vec![("filter[app]", app_id), ("limit", "200")];
if let Some(platform) = filter_platform {
query.push(("filter[platform]", platform.as_query_value()));
}
self.list_all::<PreReleaseVersionAttributes>("/v1/preReleaseVersions", &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use smbcloud_ascapi_core::jsonapi::ListDocument;
#[test]
fn default_filter_sends_no_query_pairs() {
assert!(BuildFilter::default().query_pairs().is_empty());
}
#[test]
fn each_field_maps_to_its_asc_filter_key() {
assert_eq!(
BuildFilter {
version: Some("42".into()),
..Default::default()
}
.query_pairs(),
vec![("filter[version]", "42".to_string())],
);
assert_eq!(
BuildFilter {
pre_release_version: Some("1.1.0".into()),
..Default::default()
}
.query_pairs(),
vec![("filter[preReleaseVersion.version]", "1.1.0".to_string())],
);
assert_eq!(
BuildFilter {
processing_state: Some("VALID".into()),
..Default::default()
}
.query_pairs(),
vec![("filter[processingState]", "VALID".to_string())],
);
}
#[test]
fn all_fields_set_are_all_sent() {
let pairs = BuildFilter {
version: Some("42".into()),
pre_release_version: Some("1.1.0".into()),
processing_state: Some("VALID".into()),
}
.query_pairs();
assert_eq!(
pairs,
vec![
("filter[version]", "42".to_string()),
("filter[preReleaseVersion.version]", "1.1.0".to_string()),
("filter[processingState]", "VALID".to_string()),
],
);
}
#[test]
fn deserializes_a_builds_list_response() {
let body = serde_json::json!({
"data": [
{
"id": "BUILD1",
"type": "builds",
"attributes": {
"version": "42",
"uploadedDate": "2024-01-02T03:04:05Z",
"expired": false,
"processingState": "VALID",
"minOsVersion": "17.0",
"usesNonExemptEncryption": false
}
}
]
});
let doc: ListDocument<BuildAttributes> =
serde_json::from_value(body).expect("a builds list must deserialize");
assert_eq!(doc.data.len(), 1);
let build = &doc.data[0];
assert_eq!(build.id, "BUILD1");
assert_eq!(build.attributes.version.as_deref(), Some("42"));
assert_eq!(build.attributes.processing_state.as_deref(), Some("VALID"));
assert_eq!(build.attributes.expired, Some(false));
assert_eq!(build.attributes.uses_non_exempt_encryption, Some(false));
}
#[test]
fn update_body_sends_only_the_set_attribute() {
let body = UpdateBody {
data: UpdateData {
resource_type: RESOURCE_TYPE,
id: "BUILD1".to_string(),
attributes: BuildUpdateAttributes {
uses_non_exempt_encryption: Some(false),
},
},
};
assert_eq!(
serde_json::to_value(&body).unwrap(),
serde_json::json!({
"data": {
"type": "builds",
"id": "BUILD1",
"attributes": { "usesNonExemptEncryption": false }
}
}),
);
assert_eq!(
serde_json::to_value(BuildUpdateAttributes::default()).unwrap(),
serde_json::json!({}),
);
}
#[test]
fn deserializes_a_pre_release_versions_response() {
let body = serde_json::json!({
"data": [
{
"id": "PRV1",
"type": "preReleaseVersions",
"attributes": {
"version": "1.1.0",
"platform": "IOS"
}
}
]
});
let doc: ListDocument<PreReleaseVersionAttributes> =
serde_json::from_value(body).expect("a preReleaseVersions list must deserialize");
assert_eq!(doc.data.len(), 1);
let prv = &doc.data[0];
assert_eq!(prv.attributes.version.as_deref(), Some("1.1.0"));
assert_eq!(prv.attributes.platform, Some(Platform::Ios));
}
}