apple 0.2.0

A Rust library for Apple Sign-In authentication, CloudKit Web Services, and App Store Server API
Documentation
use crate::cloudkit::client::CloudKitClient;
use crate::cloudkit::query::Query;
use crate::cloudkit::types::*;
use crate::error::AppleError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

#[derive(Debug, Serialize)]
struct ModifyRecordsRequest {
    operations: Vec<RecordOperation>,
    #[serde(rename = "zoneID", skip_serializing_if = "Option::is_none")]
    zone_id: Option<ZoneID>,
    #[serde(skip_serializing_if = "Option::is_none")]
    atomic: Option<bool>,
}

#[derive(Debug, Serialize)]
struct RecordOperation {
    #[serde(rename = "operationType")]
    operation_type: OperationType,
    record: RecordBody,
}

#[derive(Debug, Serialize)]
struct RecordBody {
    #[serde(rename = "recordName", skip_serializing_if = "Option::is_none")]
    record_name: Option<String>,
    #[serde(rename = "recordType", skip_serializing_if = "Option::is_none")]
    record_type: Option<String>,
    #[serde(rename = "recordChangeTag", skip_serializing_if = "Option::is_none")]
    record_change_tag: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    fields: Option<HashMap<String, FieldValue>>,
}

#[derive(Debug, Deserialize)]
pub struct ModifyRecordsResponse {
    pub records: Vec<RecordResult>,
}

#[derive(Debug, Deserialize)]
pub struct RecordResult {
    #[serde(rename = "recordName")]
    pub record_name: Option<String>,
    #[serde(rename = "recordType")]
    pub record_type: Option<String>,
    #[serde(rename = "recordChangeTag")]
    pub record_change_tag: Option<String>,
    #[serde(default)]
    pub fields: HashMap<String, FieldValue>,
    #[serde(rename = "zoneID")]
    pub zone_id: Option<ZoneID>,
    pub created: Option<RecordTimestamp>,
    pub modified: Option<RecordTimestamp>,
    #[serde(rename = "serverErrorCode")]
    pub server_error_code: Option<String>,
    pub reason: Option<String>,
}

#[derive(Debug, Serialize)]
struct QueryRecordsRequest {
    query: Query,
    #[serde(rename = "zoneID", skip_serializing_if = "Option::is_none")]
    zone_id: Option<ZoneID>,
    #[serde(rename = "resultsLimit", skip_serializing_if = "Option::is_none")]
    results_limit: Option<u32>,
    #[serde(rename = "continuationMarker", skip_serializing_if = "Option::is_none")]
    continuation_marker: Option<String>,
    #[serde(rename = "desiredKeys", skip_serializing_if = "Option::is_none")]
    desired_keys: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
pub struct QueryResponse {
    pub records: Vec<RecordResult>,
    #[serde(rename = "continuationMarker")]
    pub continuation_marker: Option<String>,
}

#[derive(Debug, Serialize)]
struct LookupRecordsRequest {
    records: Vec<RecordLookup>,
    #[serde(rename = "zoneID", skip_serializing_if = "Option::is_none")]
    zone_id: Option<ZoneID>,
    #[serde(rename = "desiredKeys", skip_serializing_if = "Option::is_none")]
    desired_keys: Option<Vec<String>>,
}

#[derive(Debug, Serialize)]
struct RecordLookup {
    #[serde(rename = "recordName")]
    record_name: String,
}

impl CloudKitClient {
    pub async fn create_record(
        &self,
        db: &DatabaseType,
        record: Record,
    ) -> Result<Record, AppleError> {
        let url = self.build_url(db, "records/modify");
        let fields = if record.fields.is_empty() {
            None
        } else {
            Some(record.fields)
        };
        let request = ModifyRecordsRequest {
            operations: vec![RecordOperation {
                operation_type: OperationType::Create,
                record: RecordBody {
                    record_name: record.record_name,
                    record_type: Some(record.record_type),
                    record_change_tag: None,
                    fields,
                },
            }],
            zone_id: record.zone_id,
            atomic: None,
        };

        let response: ModifyRecordsResponse = self.signed_post(&url, &request).await?;
        let result = response
            .records
            .into_iter()
            .next()
            .ok_or_else(|| AppleError::JsonError("Empty response from CloudKit".to_string()))?;

        if let Some(ref err_code) = result.server_error_code {
            return Err(AppleError::CloudKitError(
                crate::error::CloudKitErrorResponse {
                    server_error_code: crate::error::CloudKitErrorCode::parse(err_code),
                    reason: result.reason.unwrap_or_default(),
                    uuid: None,
                    retry_after: None,
                },
            ));
        }

        Ok(Record {
            record_name: result.record_name,
            record_type: result.record_type.unwrap_or_default(),
            record_change_tag: result.record_change_tag,
            fields: result.fields,
            zone_id: result.zone_id,
            created: result.created,
            modified: result.modified,
        })
    }

    pub async fn update_record(
        &self,
        db: &DatabaseType,
        record: Record,
    ) -> Result<Record, AppleError> {
        let url = self.build_url(db, "records/modify");
        let fields = if record.fields.is_empty() {
            None
        } else {
            Some(record.fields)
        };
        let request = ModifyRecordsRequest {
            operations: vec![RecordOperation {
                operation_type: OperationType::Update,
                record: RecordBody {
                    record_name: record.record_name,
                    record_type: Some(record.record_type),
                    record_change_tag: record.record_change_tag,
                    fields,
                },
            }],
            zone_id: record.zone_id,
            atomic: None,
        };

        let response: ModifyRecordsResponse = self.signed_post(&url, &request).await?;
        let result = response
            .records
            .into_iter()
            .next()
            .ok_or_else(|| AppleError::JsonError("Empty response from CloudKit".to_string()))?;

        if let Some(ref err_code) = result.server_error_code {
            return Err(AppleError::CloudKitError(
                crate::error::CloudKitErrorResponse {
                    server_error_code: crate::error::CloudKitErrorCode::parse(err_code),
                    reason: result.reason.unwrap_or_default(),
                    uuid: None,
                    retry_after: None,
                },
            ));
        }

        Ok(Record {
            record_name: result.record_name,
            record_type: result.record_type.unwrap_or_default(),
            record_change_tag: result.record_change_tag,
            fields: result.fields,
            zone_id: result.zone_id,
            created: result.created,
            modified: result.modified,
        })
    }

    pub async fn delete_record(
        &self,
        db: &DatabaseType,
        record_name: &str,
        record_type: &str,
        zone_id: Option<ZoneID>,
    ) -> Result<(), AppleError> {
        let url = self.build_url(db, "records/modify");
        let request = ModifyRecordsRequest {
            operations: vec![RecordOperation {
                operation_type: OperationType::Delete,
                record: RecordBody {
                    record_name: Some(record_name.to_string()),
                    record_type: Some(record_type.to_string()),
                    record_change_tag: None,
                    fields: None,
                },
            }],
            zone_id,
            atomic: None,
        };

        let response: ModifyRecordsResponse = self.signed_post(&url, &request).await?;
        if let Some(result) = response.records.into_iter().next()
            && let Some(ref err_code) = result.server_error_code
        {
            return Err(AppleError::CloudKitError(
                crate::error::CloudKitErrorResponse {
                    server_error_code: crate::error::CloudKitErrorCode::parse(err_code),
                    reason: result.reason.unwrap_or_default(),
                    uuid: None,
                    retry_after: None,
                },
            ));
        }

        Ok(())
    }

    pub async fn query_records(
        &self,
        db: &DatabaseType,
        query: Query,
        zone_id: Option<ZoneID>,
        results_limit: Option<u32>,
        continuation_marker: Option<String>,
        desired_keys: Option<Vec<String>>,
    ) -> Result<QueryResponse, AppleError> {
        let url = self.build_url(db, "records/query");
        let request = QueryRecordsRequest {
            query,
            zone_id,
            results_limit,
            continuation_marker,
            desired_keys,
        };

        self.signed_post(&url, &request).await
    }

    pub async fn lookup_records(
        &self,
        db: &DatabaseType,
        record_names: &[&str],
        zone_id: Option<ZoneID>,
        desired_keys: Option<Vec<String>>,
    ) -> Result<Vec<Record>, AppleError> {
        let url = self.build_url(db, "records/lookup");
        let request = LookupRecordsRequest {
            records: record_names
                .iter()
                .map(|name| RecordLookup {
                    record_name: name.to_string(),
                })
                .collect(),
            zone_id,
            desired_keys,
        };

        let response: ModifyRecordsResponse = self.signed_post(&url, &request).await?;
        Ok(response
            .records
            .into_iter()
            .map(|r| Record {
                record_name: r.record_name,
                record_type: r.record_type.unwrap_or_default(),
                record_change_tag: r.record_change_tag,
                fields: r.fields,
                zone_id: r.zone_id,
                created: r.created,
                modified: r.modified,
            })
            .collect())
    }

    pub async fn modify_records(
        &self,
        db: &DatabaseType,
        operations: Vec<(OperationType, Record)>,
        zone_id: Option<ZoneID>,
        atomic: Option<bool>,
    ) -> Result<Vec<RecordResult>, AppleError> {
        let url = self.build_url(db, "records/modify");
        let ops: Vec<RecordOperation> = operations
            .into_iter()
            .map(|(op_type, record)| {
                let fields = if record.fields.is_empty() {
                    None
                } else {
                    Some(record.fields)
                };
                RecordOperation {
                    operation_type: op_type,
                    record: RecordBody {
                        record_name: record.record_name,
                        record_type: Some(record.record_type),
                        record_change_tag: record.record_change_tag,
                        fields,
                    },
                }
            })
            .collect();

        let request = ModifyRecordsRequest {
            operations: ops,
            zone_id,
            atomic,
        };

        let response: ModifyRecordsResponse = self.signed_post(&url, &request).await?;
        Ok(response.records)
    }
}