use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use smbcloud_ascapi_core::jsonapi::{CreateBody, CreateData, Document, Resource};
use smbcloud_ascapi_core::Client;
use smbcloud_ascapi_core::Result;
pub const RESOURCE_TYPE: &str = "bundleIds";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BundleIdPlatform {
#[serde(rename = "IOS")]
Ios,
#[serde(rename = "MAC_OS")]
MacOs,
#[serde(rename = "UNIVERSAL")]
Universal,
#[serde(rename = "SERVICES")]
Services,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BundleIdAttributes {
pub name: Option<String>,
pub identifier: Option<String>,
pub platform: Option<BundleIdPlatform>,
pub seed_id: Option<String>,
}
pub type BundleId = Resource<BundleIdAttributes>;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BundleIdCreateAttributes {
pub identifier: String,
pub name: String,
pub platform: BundleIdPlatform,
}
#[async_trait]
pub trait BundleIdsApi {
async fn list_bundle_ids(&self, filter_identifier: Option<&str>) -> Result<Vec<BundleId>>;
async fn create_bundle_id(&self, attributes: BundleIdCreateAttributes) -> Result<BundleId>;
}
#[async_trait]
impl BundleIdsApi for Client {
async fn list_bundle_ids(&self, filter_identifier: Option<&str>) -> Result<Vec<BundleId>> {
let mut query = Vec::new();
if let Some(identifier) = filter_identifier {
query.push(("filter[identifier]", identifier));
}
query.push(("limit", "200"));
let bundle_ids = self
.list_all::<BundleIdAttributes>("/v1/bundleIds", &query)
.await?;
Ok(match filter_identifier {
Some(identifier) => exact_identifier(bundle_ids, identifier),
None => bundle_ids,
})
}
async fn create_bundle_id(&self, attributes: BundleIdCreateAttributes) -> Result<BundleId> {
let body = CreateBody {
data: CreateData {
resource_type: RESOURCE_TYPE,
attributes,
relationships: None::<()>,
},
};
let doc: Document<BundleIdAttributes> = self
.request(Method::POST, "/v1/bundleIds", &[], Some(&body))
.await?;
Ok(doc.data)
}
}
fn exact_identifier(bundle_ids: Vec<BundleId>, identifier: &str) -> Vec<BundleId> {
bundle_ids
.into_iter()
.filter(|bundle_id| bundle_id.attributes.identifier.as_deref() == Some(identifier))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use smbcloud_ascapi_core::jsonapi::ListDocument;
#[test]
fn an_identifier_filter_keeps_only_the_exact_match() {
let doc: ListDocument<BundleIdAttributes> = serde_json::from_value(serde_json::json!({
"data": [
{ "id": "A", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAIAuth", "platform": "SERVICES" } },
{ "id": "B", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAI", "platform": "UNIVERSAL" } },
{ "id": "C", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAI.topshelf", "platform": "UNIVERSAL" } },
{ "id": "D", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitFireAIWatch", "platform": "UNIVERSAL" } }
]
}))
.unwrap();
let exact = exact_identifier(doc.data.clone(), "ai.splitfire.SplitfireAI");
assert_eq!(
exact.iter().map(|b| b.id.as_str()).collect::<Vec<_>>(),
["B"]
);
assert!(exact_identifier(doc.data, "ai.splitfire.Splitfire").is_empty());
}
#[test]
fn a_services_identifier_does_not_break_the_list() {
let body = serde_json::json!({
"data": [
{
"id": "AAA",
"type": "bundleIds",
"attributes": {
"name": "An app",
"identifier": "xyz.example.app",
"platform": "UNIVERSAL",
"seedId": "TEAMID"
}
},
{
"id": "BBB",
"type": "bundleIds",
"attributes": {
"name": "Sign in with Apple",
"identifier": "xyz.example.service",
"platform": "SERVICES",
"seedId": "TEAMID"
}
}
]
});
let doc: ListDocument<BundleIdAttributes> =
serde_json::from_value(body).expect("a Services identifier must not fail the list");
assert_eq!(doc.data.len(), 2);
assert_eq!(
doc.data[1].attributes.platform,
Some(BundleIdPlatform::Services)
);
}
}