use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use smbcloud_ascapi_core::jsonapi::{Document, ListDocument, Resource, ResourceId, ToOne};
use smbcloud_ascapi_core::Client;
use smbcloud_ascapi_core::Result;
pub const RESOURCE_TYPE: &str = "betaAppReviewSubmissions";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BetaReviewState {
WaitingForReview,
InReview,
Rejected,
Approved,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BetaAppReviewSubmissionAttributes {
pub beta_review_state: Option<BetaReviewState>,
pub submitted_date: Option<String>,
}
pub type BetaAppReviewSubmission = Resource<BetaAppReviewSubmissionAttributes>;
#[derive(Debug, Clone, Serialize)]
struct CreateBody {
data: CreateData,
}
#[derive(Debug, Clone, Serialize)]
struct CreateData {
#[serde(rename = "type")]
resource_type: &'static str,
relationships: CreateRelationships,
}
#[derive(Debug, Clone, Serialize)]
struct CreateRelationships {
build: ToOne,
}
#[async_trait]
pub trait BetaAppReviewSubmissionsApi {
async fn submit_build_for_beta_review(&self, build_id: &str)
-> Result<BetaAppReviewSubmission>;
async fn list_beta_app_review_submissions(
&self,
build_id: &str,
) -> Result<Vec<BetaAppReviewSubmission>>;
}
#[async_trait]
impl BetaAppReviewSubmissionsApi for Client {
async fn submit_build_for_beta_review(
&self,
build_id: &str,
) -> Result<BetaAppReviewSubmission> {
let body = CreateBody {
data: CreateData {
resource_type: RESOURCE_TYPE,
relationships: CreateRelationships {
build: ToOne {
data: ResourceId {
resource_type: crate::build::RESOURCE_TYPE,
id: build_id.to_string(),
},
},
},
},
};
let doc: Document<BetaAppReviewSubmissionAttributes> = self
.request(
Method::POST,
"/v1/betaAppReviewSubmissions",
&[],
Some(&body),
)
.await?;
Ok(doc.data)
}
async fn list_beta_app_review_submissions(
&self,
build_id: &str,
) -> Result<Vec<BetaAppReviewSubmission>> {
let path = "/v1/betaAppReviewSubmissions";
let query = &[("filter[build]", build_id)];
let doc: ListDocument<BetaAppReviewSubmissionAttributes> =
self.request(Method::GET, path, query, None::<&()>).await?;
Ok(doc.data)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deserializes_a_submission_list() {
let body = serde_json::json!({
"data": [
{
"id": "SUB1",
"type": "betaAppReviewSubmissions",
"attributes": {
"betaReviewState": "WAITING_FOR_REVIEW",
"submittedDate": "2025-01-01T00:00:00.000+0000"
}
}
]
});
let doc: ListDocument<BetaAppReviewSubmissionAttributes> =
serde_json::from_value(body).expect("submission list must deserialize");
assert_eq!(doc.data.len(), 1);
let sub = &doc.data[0];
assert_eq!(sub.id, "SUB1");
assert_eq!(
sub.attributes.beta_review_state,
Some(BetaReviewState::WaitingForReview)
);
assert_eq!(
sub.attributes.submitted_date.as_deref(),
Some("2025-01-01T00:00:00.000+0000")
);
}
#[test]
fn deserializes_approved_state() {
let body = serde_json::json!({
"data": {
"id": "SUB2",
"type": "betaAppReviewSubmissions",
"attributes": {
"betaReviewState": "APPROVED",
"submittedDate": "2025-02-01T10:00:00.000+0000"
}
}
});
let doc: Document<BetaAppReviewSubmissionAttributes> =
serde_json::from_value(body).expect("single submission must deserialize");
assert_eq!(
doc.data.attributes.beta_review_state,
Some(BetaReviewState::Approved)
);
}
#[test]
fn filter_key_is_exact() {
let query: Vec<(&str, &str)> = vec![("filter[build]", "BUILD_ID")];
assert_eq!(query[0].0, "filter[build]");
}
#[test]
fn create_body_omits_attributes() {
let body = CreateBody {
data: CreateData {
resource_type: RESOURCE_TYPE,
relationships: CreateRelationships {
build: ToOne {
data: ResourceId {
resource_type: crate::build::RESOURCE_TYPE,
id: "BUILD_ID".to_string(),
},
},
},
},
};
let v = serde_json::to_value(&body).unwrap();
assert!(
v["data"].get("attributes").is_none(),
"attributes must be omitted, not null: {v}"
);
assert_eq!(v["data"]["type"], RESOURCE_TYPE);
assert_eq!(
v["data"]["relationships"]["build"]["data"]["id"],
"BUILD_ID"
);
}
}