cnctd_appstore 0.1.0

App Store Connect API client for TestFlight, builds, and submissions
Documentation
use anyhow::{anyhow, Result};
use serde::Deserialize;

use crate::client::{AppStoreConnectClient, AscListResponse, AscResponse, BASE_URL};

/// Build data from App Store Connect
#[derive(Debug, Deserialize, Clone)]
pub struct BuildData {
    pub id: String,
    #[serde(rename = "type")]
    pub data_type: String,
    pub attributes: BuildAttributes,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct BuildAttributes {
    pub version: Option<String>,
    pub uploaded_date: Option<String>,
    pub expiration_date: Option<String>,
    pub processing_state: Option<String>,
    pub build_audience_type: Option<String>,
    pub min_os_version: Option<String>,
    pub icon_asset_token: Option<serde_json::Value>,
}

/// Pre-release version data
#[derive(Debug, Deserialize, Clone)]
pub struct PreReleaseVersionData {
    pub id: String,
    #[serde(rename = "type")]
    pub data_type: String,
    pub attributes: PreReleaseVersionAttributes,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PreReleaseVersionAttributes {
    pub version: Option<String>,
    pub platform: Option<String>,
}

impl AppStoreConnectClient {
    /// List builds for an app, sorted by most recent first
    pub async fn list_builds(&self, app_id: &str, limit: Option<u32>) -> Result<Vec<BuildData>> {
        let token = self.generate_token()?;
        let limit = limit.unwrap_or(10);

        let url = format!(
            "{}/builds?filter[app]={}&sort=-uploadedDate&limit={}",
            BASE_URL, app_id, limit
        );

        let response = self.client
            .get(&url)
            .header("Authorization", format!("Bearer {}", token))
            .send()
            .await
            .map_err(|e| anyhow!("Failed to list builds: {}", e))?;

        let status = response.status();
        if status.is_success() {
            let result: AscListResponse<BuildData> = response.json().await
                .map_err(|e| anyhow!("Failed to parse builds response: {}", e))?;
            Ok(result.data)
        } else {
            let error_body = response.text().await.unwrap_or_default();
            Err(anyhow!("{}", Self::parse_error(status, &error_body)))
        }
    }

    /// Get a specific build by ID
    pub async fn get_build(&self, build_id: &str) -> Result<BuildData> {
        let token = self.generate_token()?;

        let url = format!("{}/builds/{}", BASE_URL, build_id);

        let response = self.client
            .get(&url)
            .header("Authorization", format!("Bearer {}", token))
            .send()
            .await
            .map_err(|e| anyhow!("Failed to get build: {}", e))?;

        let status = response.status();
        if status.is_success() {
            let result: AscResponse<BuildData> = response.json().await
                .map_err(|e| anyhow!("Failed to parse build response: {}", e))?;
            Ok(result.data)
        } else {
            let error_body = response.text().await.unwrap_or_default();
            Err(anyhow!("{}", Self::parse_error(status, &error_body)))
        }
    }

    /// List pre-release versions (TestFlight versions) for an app
    pub async fn list_prerelease_versions(&self, app_id: &str) -> Result<Vec<PreReleaseVersionData>> {
        let token = self.generate_token()?;

        let url = format!(
            "{}/preReleaseVersions?filter[app]={}&sort=-version",
            BASE_URL, app_id
        );

        let response = self.client
            .get(&url)
            .header("Authorization", format!("Bearer {}", token))
            .send()
            .await
            .map_err(|e| anyhow!("Failed to list pre-release versions: {}", e))?;

        let status = response.status();
        if status.is_success() {
            let result: AscListResponse<PreReleaseVersionData> = response.json().await
                .map_err(|e| anyhow!("Failed to parse pre-release versions response: {}", e))?;
            Ok(result.data)
        } else {
            let error_body = response.text().await.unwrap_or_default();
            Err(anyhow!("{}", Self::parse_error(status, &error_body)))
        }
    }
}