ferrtable 0.0.2

Ferris the crab's favorite Airtable library
Documentation
use std::fmt::Debug;

use derive_builder::Builder;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use crate::{client::Client, errors::ExecutionError, types::AirtableRecord};

#[derive(Builder, Clone, Debug)]
#[builder(pattern = "owned", setter(prefix = "with"))]
pub struct CreateRecordsQuery<T>
where
    T: Serialize,
{
    base_id: String,

    #[builder(vis = "pub(crate)")]
    client: Client,

    #[builder(vis = "pub(crate)")]
    records: Vec<T>,

    table_id: String,
}

#[derive(Clone, Deserialize)]
pub struct CreateRecordsResponse<T>
where
    T: Clone + Debug + Serialize,
{
    /// Records successfully created in Airtable.
    pub records: Vec<AirtableRecord<T>>,

    /// Additional information, present if the operations only partially succeed.
    pub details: Option<CreateRecordsDetails>,
}

#[derive(Clone, Debug, Deserialize)]
pub struct CreateRecordsDetails {
    /// Expected value is "partialSuccess".
    pub message: String,

    /// Expected values are "attachmentsFailedUploading", "attachmentUploadRateIsTooHigh".
    pub reasons: Vec<String>,
}

impl<T> CreateRecordsQuery<T>
where
    T: Clone + Debug + DeserializeOwned + Serialize,
{
    /// Execute the API request.
    ///
    /// Currently, failures return a one-size-fits-all error wrapping the
    /// underlying `reqwest::Error`. This may be improved in future releases
    /// to better differentiate between network, serialization, deserialization,
    /// and API errors.
    pub async fn execute(self) -> Result<CreateRecordsResponse<T>, ExecutionError> {
        #[derive(Serialize)]
        struct Record<RT: Serialize> {
            fields: RT,
        }
        #[derive(Serialize)]
        struct RequestBody<RT: Serialize> {
            records: Vec<Record<RT>>,
        }
        let base_id = utf8_percent_encode(&self.base_id, NON_ALPHANUMERIC).to_string();
        let table_id = utf8_percent_encode(&self.table_id, NON_ALPHANUMERIC).to_string();
        let http_resp = self
            .client
            .post_path(&format!("v0/{base_id}/{table_id}"))
            .json(&RequestBody {
                records: self
                    .records
                    .into_iter()
                    .map(|rec| Record { fields: rec })
                    .collect(),
            })
            .send()
            .await?
            .error_for_status()?;
        let deserialized_resp: CreateRecordsResponse<T> = http_resp.json().await?;
        Ok(deserialized_resp)
    }
}